diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 51141015c6f..53bd1608007 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -216,6 +216,8 @@ 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) @@ -223,8 +225,6 @@ dependencies { 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 @@ -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) @@ -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) diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt index 3326c2f3dc3..e34c9f34b94 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt @@ -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 @@ -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 @@ -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 @@ -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) @@ -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() } /** @@ -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() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsAdapter.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsAdapter.kt index 4233d950877..6642dd08009 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsAdapter.kt @@ -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 @@ -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) } @@ -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) diff --git a/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.kt b/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.kt index 0198c61a5e0..f5ec401fe00 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.kt @@ -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 @@ -71,14 +74,17 @@ class GridViewAdapter( ) val item = data?.get(position) - val imageView = view.findViewById(R.id.categoryImageView) + val imageView = view.findViewById(R.id.categoryImageView) val fileName = view.findViewById(R.id.categoryImageTitle) val uploader = view.findViewById(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 diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.kt index 899ef458f6e..1e4eb645e69 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.kt @@ -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 @@ -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() } @@ -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) } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/SetWallpaperWorker.kt b/app/src/main/java/fr/free/nrw/commons/contributions/SetWallpaperWorker.kt index 8e899fcbab6..30503ed28e3 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/SetWallpaperWorker.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/SetWallpaperWorker.kt @@ -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?>) { - 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) { diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt index 87f68a3e138..6d71979004b 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt @@ -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 @@ -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 { diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt index aca4fb94bf7..44c6ec19531 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -10,7 +10,8 @@ import android.widget.Toast import androidx.constraintlayout.widget.Group import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.Glide +import coil3.load +import coil3.request.crossfade import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView import fr.free.nrw.commons.R import fr.free.nrw.commons.contributions.Contribution @@ -223,22 +224,20 @@ class ImageAdapter( val actionableImages: List = ArrayList(actionableImagesMap.values) if (actionableImages.size > position) { image = actionableImages[position] - Glide - .with(holder.image) - .load(image.uri) - .thumbnail(0.3f) - .into(holder.image) + holder.image.load(image.uri) { + crossfade(true) + size(coil3.size.Size.ORIGINAL) + } } } // If switch is turned off, it just fetches the image from all images without any // further operations } else { - Glide - .with(holder.image) - .load(image.uri) - .thumbnail(0.3f) - .into(holder.image) + holder.image.load(image.uri) { + crossfade(true) + size(coil3.size.Size.ORIGINAL) + } } } @@ -285,11 +284,10 @@ class ImageAdapter( alreadyAddedPositions.add(imagePositionAsPerIncreasingOrder) imagePositionAsPerIncreasingOrder++ _currentImagesCount.value = imagePositionAsPerIncreasingOrder - Glide - .with(holder.image) - .load(allImages[next].uri) - .thumbnail(0.3f) - .into(holder.image) + holder.image.load(allImages[next].uri) { + crossfade(true) + size(coil3.size.Size.ORIGINAL) + } notifyItemInserted(position) notifyItemRangeChanged(position, itemCount + 1) } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/DepictionAdapter.kt b/app/src/main/java/fr/free/nrw/commons/explore/depictions/DepictionAdapter.kt index 56baacc6593..593ee3dbd96 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/DepictionAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/DepictionAdapter.kt @@ -2,6 +2,8 @@ package fr.free.nrw.commons.explore.depictions import android.view.LayoutInflater import android.view.ViewGroup +import coil3.load +import coil3.request.placeholder import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView @@ -38,9 +40,11 @@ class DepictedItemViewHolder( depictsLabel.text = item.name description.text = item.description if (item.imageUrl?.isNotBlank() == true) { - depictsImage.setImageURI(item.imageUrl) + depictsImage.load(item.imageUrl) { + placeholder(R.drawable.ic_wikidata_logo_24dp) + } } else { - depictsImage.setActualImageResource(R.drawable.ic_wikidata_logo_24dp) + depictsImage.setImageResource(R.drawable.ic_wikidata_logo_24dp) } } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.kt b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.kt index 0873572d1d9..6d443d30389 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.kt @@ -3,12 +3,12 @@ package fr.free.nrw.commons.explore.map import android.content.Context import android.content.res.Resources import android.graphics.Bitmap -import android.graphics.drawable.Drawable import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat -import com.bumptech.glide.Glide -import com.bumptech.glide.request.RequestOptions -import com.bumptech.glide.request.target.CustomTarget -import com.bumptech.glide.request.transition.Transition +import coil3.SingletonImageLoader +import coil3.request.ImageRequest +import coil3.request.allowHardware +import coil3.request.target +import coil3.toBitmap import fr.free.nrw.commons.BaseMarker import fr.free.nrw.commons.MapController import fr.free.nrw.commons.Media @@ -173,18 +173,15 @@ class ExploreMapController @Inject constructor( ) baseMarker.place = explorePlace - Glide.with(context) - .asBitmap() - .load(explorePlace.thumb) - .placeholder(R.drawable.image_placeholder_96) - .apply(RequestOptions().override(96, 96).centerCrop()) - .into(object : CustomTarget() { - // We add icons to markers when bitmaps are ready - override fun onResourceReady( - resource: Bitmap, - transition: Transition? - ) { - baseMarker.icon = addRedBorder(resource, 6, context) + val imageLoader = SingletonImageLoader.get(context) + val request = ImageRequest.Builder(context) + .data(explorePlace.thumb) + .size(96, 96) + .allowHardware(false) + .target( + onSuccess = { image -> + val bitmap = image.toBitmap() + baseMarker.icon = addRedBorder(bitmap, 6, context) baseMarkerList.add(baseMarker) if (baseMarkerList.size == placeList.size) { // if true, we added all markers to list and can trigger thumbs ready callback @@ -193,13 +190,8 @@ class ExploreMapController @Inject constructor( explorePlacesInfo ) } - } - - override fun onLoadCleared(placeholder: Drawable?) = Unit - - // We add thumbnail icon for images that couldn't be loaded - override fun onLoadFailed(errorDrawable: Drawable?) { - super.onLoadFailed(errorDrawable) + }, + onError = { _ -> baseMarker.fromResource(context, R.drawable.image_placeholder_96) baseMarkerList.add(baseMarker) if (baseMarkerList.size == placeList.size) { @@ -210,7 +202,9 @@ class ExploreMapController @Inject constructor( ) } } - }) + ) + .build() + imageLoader.enqueue(request) } } return baseMarkerList diff --git a/app/src/main/java/fr/free/nrw/commons/explore/media/PagedMediaAdapter.kt b/app/src/main/java/fr/free/nrw/commons/explore/media/PagedMediaAdapter.kt index 521ba77c668..9424591aa8f 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/media/PagedMediaAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/media/PagedMediaAdapter.kt @@ -2,6 +2,9 @@ package fr.free.nrw.commons.explore.media import android.view.View import android.view.ViewGroup +import coil3.load +import coil3.request.placeholder +import coil3.request.error import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.DiffUtil import fr.free.nrw.commons.Media @@ -78,7 +81,10 @@ class SearchImagesViewHolder( val media = item.first binding.categoryImageView.setOnClickListener { onImageClicked(item.second) } binding.categoryImageTitle.text = media.mostRelevantCaption - binding.categoryImageView.setImageURI(media.thumbUrl) + binding.categoryImageView.load(media.thumbUrl) { + placeholder(R.drawable.image_placeholder) + error(R.drawable.image_placeholder) + } setAuthorText(media) } diff --git a/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.kt b/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.kt deleted file mode 100644 index c8de4022b30..00000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.kt +++ /dev/null @@ -1,199 +0,0 @@ -package fr.free.nrw.commons.media - -import android.os.Looper -import android.os.SystemClock -import com.facebook.imagepipeline.common.BytesRange -import com.facebook.imagepipeline.image.EncodedImage -import com.facebook.imagepipeline.producers.BaseNetworkFetcher -import com.facebook.imagepipeline.producers.BaseProducerContextCallbacks -import com.facebook.imagepipeline.producers.Consumer -import com.facebook.imagepipeline.producers.FetchState -import com.facebook.imagepipeline.producers.NetworkFetcher -import com.facebook.imagepipeline.producers.ProducerContext -import fr.free.nrw.commons.CommonsApplication -import fr.free.nrw.commons.kvstore.JsonKvStore -import okhttp3.CacheControl -import okhttp3.Call -import okhttp3.Callback -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import timber.log.Timber -import java.io.IOException -import java.util.concurrent.Executor -import javax.inject.Inject -import javax.inject.Named -import javax.inject.Singleton - -// Custom implementation of Fresco's Network fetcher to skip downloading of images when limited connection mode is enabled -// https://github.com/facebook/fresco/blob/master/imagepipeline-backends/imagepipeline-okhttp3/src/main/java/com/facebook/imagepipeline/backends/okhttp3/OkHttpNetworkFetcher.java -@Singleton -class CustomOkHttpNetworkFetcher -@JvmOverloads constructor( - private val mCallFactory: Call.Factory, - private val mCancellationExecutor: Executor, - private val defaultKvStore: JsonKvStore, - disableOkHttpCache: Boolean = true -) : BaseNetworkFetcher() { - - private val mCacheControl = - if (disableOkHttpCache) CacheControl.Builder().noStore().build() else null - private val isLimitedConnectionMode: Boolean - get() = defaultKvStore.getBoolean( - CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, - false - ) - - /** - * @param okHttpClient client to use - */ - @Inject - constructor( - okHttpClient: OkHttpClient, - @Named("default_preferences") defaultKvStore: JsonKvStore - ) : this(okHttpClient, okHttpClient.dispatcher.executorService, defaultKvStore) - - /** - * @param mCallFactory custom [Call.Factory] for fetching image from the network - * @param mCancellationExecutor executor on which fetching cancellation is performed if - * cancellation is requested from the UI Thread - * @param disableOkHttpCache true if network requests should not be cached by OkHttp - */ - override fun createFetchState(consumer: Consumer, context: ProducerContext) = - OkHttpNetworkFetchState(consumer, context) - - override fun fetch( - fetchState: OkHttpNetworkFetchState, callback: NetworkFetcher.Callback - ) { - fetchState.submitTime = SystemClock.elapsedRealtime() - - try { - if (isLimitedConnectionMode) { - Timber.d("Skipping loading of image as limited connection mode is enabled") - callback.onFailure(Exception("Failing image request as limited connection mode is enabled")) - return - } - - val requestBuilder = Request.Builder().url(fetchState.uri.toString()).get() - - if (mCacheControl != null) { - requestBuilder.cacheControl(mCacheControl) - } - - val bytesRange = fetchState.context.imageRequest.bytesRange - if (bytesRange != null) { - requestBuilder.addHeader("Range", bytesRange.toHttpRangeHeaderValue()) - } - - fetchWithRequest(fetchState, callback, requestBuilder.build()) - } catch (e: Exception) { - // handle error while creating the request - callback.onFailure(e) - } - } - - override fun onFetchCompletion(fetchState: OkHttpNetworkFetchState, byteSize: Int) { - fetchState.fetchCompleteTime = SystemClock.elapsedRealtime() - } - - override fun getExtraMap(fetchState: OkHttpNetworkFetchState, byteSize: Int) = - fetchState.toExtraMap(byteSize) - - private fun fetchWithRequest( - fetchState: OkHttpNetworkFetchState, callback: NetworkFetcher.Callback, request: Request - ) { - val call = mCallFactory.newCall(request) - - fetchState.context.addCallbacks(object : BaseProducerContextCallbacks() { - override fun onCancellationRequested() { - onFetchCancellationRequested(call) - } - }) - - call.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) = - onFetchResponse(fetchState, call, response, callback) - - override fun onFailure(call: Call, e: IOException) = - handleException(call, e, callback) - }) - } - - private fun onFetchCancellationRequested(call: Call) { - if (Looper.myLooper() != Looper.getMainLooper()) { - call.cancel() - } else { - mCancellationExecutor.execute { call.cancel() } - } - } - - private fun onFetchResponse( - fetchState: OkHttpNetworkFetchState, - call: Call, - response: Response, - callback: NetworkFetcher.Callback - ) { - fetchState.responseTime = SystemClock.elapsedRealtime() - try { - response.body.use { body -> - if (!response.isSuccessful) { - handleException(call, IOException("Unexpected HTTP code $response"), callback) - return - } - val responseRange = - BytesRange.fromContentRangeHeader(response.header("Content-Range")) - if (responseRange != null && !(responseRange.from == 0 && responseRange.to == BytesRange.TO_END_OF_CONTENT)) { - // Only treat as a partial image if the range is not all of the content - fetchState.responseBytesRange = responseRange - fetchState.onNewResultStatusFlags = Consumer.IS_PARTIAL_RESULT - } - - var contentLength = body!!.contentLength() - if (contentLength < 0) { - contentLength = 0 - } - callback.onResponse(body.byteStream(), contentLength.toInt()) - } - } catch (e: Exception) { - handleException(call, e, callback) - } - } - - /** - * Handles exceptions. - * - * OkHttp notifies callers of cancellations via an IOException. If IOException is caught - * after request cancellation, then the exception is interpreted as successful cancellation and - * onCancellation is called. Otherwise onFailure is called. - */ - private fun handleException(call: Call, e: Exception, callback: NetworkFetcher.Callback) { - if (call.isCanceled()) { - callback.onCancellation() - } else { - callback.onFailure(e) - } - } -} - -class OkHttpNetworkFetchState( - consumer: Consumer?, producerContext: ProducerContext? -) : FetchState(consumer, producerContext) { - var submitTime: Long = 0 - var responseTime: Long = 0 - var fetchCompleteTime: Long = 0 - - fun toExtraMap(byteSize: Int) = buildMap { - put(QUEUE_TIME, (responseTime - submitTime).toString()) - put(FETCH_TIME, (fetchCompleteTime - responseTime).toString()) - put(TOTAL_TIME, (fetchCompleteTime - submitTime).toString()) - put(IMAGE_SIZE, byteSize.toString()) - } - - companion object { - private const val QUEUE_TIME = "queue_time" - private const val FETCH_TIME = "fetch_time" - private const val TOTAL_TIME = "total_time" - private const val IMAGE_SIZE = "image_size" - } -} - diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt index 1ae954293fb..bf1f9650d3f 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt @@ -1,12 +1,14 @@ package fr.free.nrw.commons.media +import android.R.attr.thumbnail import android.annotation.SuppressLint import android.app.AlertDialog import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.res.Configuration -import android.graphics.drawable.Animatable +import coil3.load +import coil3.request.error import android.net.Uri import android.os.Bundle import android.text.Editable @@ -64,12 +66,8 @@ import androidx.compose.ui.unit.sp import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.viewModels -import com.facebook.drawee.backends.pipeline.Fresco -import com.facebook.drawee.controller.BaseControllerListener -import com.facebook.drawee.controller.ControllerListener -import com.facebook.drawee.interfaces.DraweeController -import com.facebook.imagepipeline.image.ImageInfo -import com.facebook.imagepipeline.request.ImageRequest +import coil3.request.placeholder +import coil3.result import fr.free.nrw.commons.BuildConfig import fr.free.nrw.commons.CameraPosition import fr.free.nrw.commons.CommonsApplication @@ -209,7 +207,8 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C * However unlike categories depictions is multi-lingual * Ex: key: en value: monument */ - private var imageInfoCache: ImageInfo? = null + private var cachedImageWidth: Int = 0 + private var cachedImageHeight: Int = 0 private var oldWidthOfImageView: Int = 0 private var newWidthOfImageView: Int = 0 private var heightVerifyingBoolean: Boolean = true // helps in maintaining aspect ratio @@ -675,7 +674,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C } /** - * The imageSpacer is Basically a transparent overlay for the SimpleDraweeView + * The imageSpacer is basically a transparent overlay for the ImageView * which holds the image to be displayed( moreover this image is out of * the scroll view ) * @@ -687,8 +686,8 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C * @param scrollWidth the current width of the scrollView */ private fun updateAspectRatio(scrollWidth: Int) { - if (imageInfoCache != null) { - var finalHeight: Int = (scrollWidth * imageInfoCache!!.height) / imageInfoCache!!.width + if (cachedImageWidth > 0 && cachedImageHeight > 0) { + var finalHeight: Int = (scrollWidth * cachedImageHeight) / cachedImageWidth val params: ViewGroup.LayoutParams = binding.mediaDetailImageView.layoutParams val spacerParams: ViewGroup.LayoutParams = binding.mediaDetailImageViewSpacer.layoutParams @@ -709,27 +708,11 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C } } - private val aspectRatioListener: ControllerListener = - object : BaseControllerListener() { - override fun onIntermediateImageSet(id: String, imageInfo: ImageInfo?) { - imageInfoCache = imageInfo - updateAspectRatio(binding.mediaDetailScrollView.width) - } - - override fun onFinalImageSet( - id: String, - imageInfo: ImageInfo?, - animatable: Animatable? - ) { - imageInfoCache = imageInfo - updateAspectRatio(binding.mediaDetailScrollView.width) - } - } - /** - * Uses two image sources. - * - low resolution thumbnail is shown initially - * - when the high resolution image is available, it replaces the low resolution image + * Loads the media image into the detail ImageView. + * + * Mirrors the original Fresco behaviour: show a spinner while loading, + * prefer the full-resolution URL, fall back to the thumbnail URL. */ private fun setupImageView() { val imageBackgroundColor: Int = imageBackgroundColor @@ -737,17 +720,32 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C binding.mediaDetailImageView.setBackgroundColor(imageBackgroundColor) } - binding.mediaDetailImageView.hierarchy.setPlaceholderImage(R.drawable.image_placeholder) - binding.mediaDetailImageView.hierarchy.setFailureImage(R.drawable.image_placeholder) + binding.mediaDetailImageView.setImageDrawable(null) + binding.mediaDetailImageProgress.visibility = View.VISIBLE - val controller: DraweeController = Fresco.newDraweeControllerBuilder() - .setLowResImageRequest(ImageRequest.fromUri(if (media != null) media!!.thumbUrl else null)) - .setRetainImageOnFailure(true) - .setImageRequest(ImageRequest.fromUri(if (media != null) media!!.imageUrl else null)) - .setControllerListener(aspectRatioListener) - .setOldController(binding.mediaDetailImageView.controller) - .build() - binding.mediaDetailImageView.controller = controller + // TODO: load low-resolution image until the full-resolution image is loaded. + binding.mediaDetailImageView.load(media!!.imageUrl) { + error(R.drawable.image_placeholder) + listener( + onSuccess = { _, _ -> + binding.mediaDetailImageProgress.visibility = View.GONE + updateImageDimensions() + updateAspectRatio(binding.mediaDetailScrollView.width) + }, + onError = { _, _ -> + binding.mediaDetailImageProgress.visibility = View.GONE + } + ) + } + } + + /** Reads the current drawable dimensions into the cached size fields. */ + private fun updateImageDimensions() { + val d = binding.mediaDetailImageView.drawable + if (d != null) { + cachedImageWidth = d.intrinsicWidth + cachedImageHeight = d.intrinsicHeight + } } private fun updateToDoWarning() { diff --git a/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt b/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt index dc6e2377844..59e72a02bd6 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt @@ -4,7 +4,6 @@ import android.app.Activity import android.app.Dialog import android.content.Intent import android.content.SharedPreferences -import android.graphics.drawable.Animatable import android.net.Uri import android.os.Bundle import android.view.View @@ -15,14 +14,7 @@ import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.ViewModelProvider -import com.facebook.drawee.backends.pipeline.Fresco -import com.facebook.drawee.controller.BaseControllerListener -import com.facebook.drawee.controller.ControllerListener -import com.facebook.drawee.drawable.ProgressBarDrawable -import com.facebook.drawee.drawable.ScalingUtils -import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder -import com.facebook.drawee.interfaces.DraweeController -import com.facebook.imagepipeline.image.ImageInfo +import coil3.load import fr.free.nrw.commons.R import fr.free.nrw.commons.customselector.database.NotForUploadStatus import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao @@ -37,7 +29,6 @@ import fr.free.nrw.commons.customselector.model.Result import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorViewModel import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorViewModelFactory import fr.free.nrw.commons.databinding.ActivityZoomableBinding -import fr.free.nrw.commons.media.zoomControllers.zoomable.DoubleTapGestureListener import fr.free.nrw.commons.theme.BaseActivity import fr.free.nrw.commons.upload.FileProcessor import fr.free.nrw.commons.upload.FileUtilsWrapper @@ -281,7 +272,7 @@ class ZoomableActivity : BaseActivity() { * Handles down swipe action */ private fun onDownSwiped() { - if (!binding.zoomable.getZoomableController().isIdentity()) { + if (binding.zoomable.scale > 1.0f) { return } @@ -351,7 +342,7 @@ class ZoomableActivity : BaseActivity() { * Handles up swipe action */ private fun onUpSwiped() { - if (!binding.zoomable.getZoomableController().isIdentity()) { + if (binding.zoomable.scale > 1.0f) { return } @@ -424,7 +415,7 @@ class ZoomableActivity : BaseActivity() { * Handles right swipe action */ private fun onRightSwiped(showAlreadyActionedImages: Boolean) { - if (!binding.zoomable.getZoomableController().isIdentity()) { + if (binding.zoomable.scale > 1.0f) { return } @@ -461,7 +452,7 @@ class ZoomableActivity : BaseActivity() { * Handles left swipe action */ private fun onLeftSwiped(showAlreadyActionedImages: Boolean) { - if (!binding.zoomable.getZoomableController().isIdentity()) { + if (binding.zoomable.scale > 1.0f) { return } @@ -615,62 +606,25 @@ class ZoomableActivity : BaseActivity() { image: Image, ): Int = list!!.indexOf(image) - /** - * Two types of loading indicators have been added to the zoom activity: - * 1. An Indeterminate spinner for showing the time lapsed between dispatch of the image request - * and starting to receiving the image. - * 2. ProgressBarDrawable that reflects how much image has been downloaded - */ - private val loadingListener: ControllerListener = - object : BaseControllerListener() { - override fun onSubmit( - id: String, - callerContext: Any, - ) { - // Sometimes the spinner doesn't appear when rapidly switching between images, this fixes that - binding.zoomProgressBar.visibility = View.VISIBLE - } - - override fun onIntermediateImageSet( - id: String, - imageInfo: ImageInfo?, - ) { - binding.zoomProgressBar.visibility = View.GONE - } + private fun init(imageUri: Uri?) { + if (imageUri != null) { + binding.zoomProgressBar.visibility = View.VISIBLE - override fun onFinalImageSet( - id: String, - imageInfo: ImageInfo?, - animatable: Animatable?, - ) { - binding.zoomProgressBar.visibility = View.GONE + // PhotoView provides built-in zoom/pan + binding.zoomable.setOnPhotoTapListener { _, _, _ -> + toggleSystemBars() } - } - private fun init(imageUri: Uri?) { - if (imageUri != null) { - val hierarchy = - GenericDraweeHierarchyBuilder - .newInstance(resources) - .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER) - .setProgressBarImage(ProgressBarDrawable()) - .setProgressBarImageScaleType(ScalingUtils.ScaleType.FIT_CENTER) - .build() - with(binding.zoomable) { - setHierarchy(hierarchy) - setAllowTouchInterceptionWhileZoomed(true) - setIsLongpressEnabled(false) - setTapListener(DoubleTapGestureListener(this) { - toggleSystemBars() - }) + binding.zoomable.load(imageUri) { + listener( + onSuccess = { _, _ -> + binding.zoomProgressBar.visibility = View.GONE + }, + onError = { _, _ -> + binding.zoomProgressBar.visibility = View.GONE + }, + ) } - val controller: DraweeController = - Fresco - .newDraweeControllerBuilder() - .setUri(imageUri) - .setControllerListener(loadingListener) - .build() - binding.zoomable.controller = controller if (photoBackgroundColor != null) { binding.zoomable.setBackgroundColor(photoBackgroundColor!!) diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/gestures/MultiPointerGestureDetector.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/gestures/MultiPointerGestureDetector.kt deleted file mode 100644 index 84dccfc0732..00000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/gestures/MultiPointerGestureDetector.kt +++ /dev/null @@ -1,252 +0,0 @@ -package fr.free.nrw.commons.media.zoomControllers.gestures - -import android.view.MotionEvent - -/** - * Component that detects and tracks multiple pointers based on touch events. - * - * Each time a pointer gets pressed or released, the current gesture (if any) will end, and a new - * one will be started (if there are still pressed pointers left). It is guaranteed that the number - * of pointers within the single gesture will remain the same during the whole gesture. - */ -open class MultiPointerGestureDetector { - - /** The listener for receiving notifications when gestures occur. */ - interface Listener { - /** A callback called right before the gesture is about to start. */ - fun onGestureBegin(detector: MultiPointerGestureDetector) - - /** A callback called each time the gesture gets updated. */ - fun onGestureUpdate(detector: MultiPointerGestureDetector) - - /** A callback called right after the gesture has finished. */ - fun onGestureEnd(detector: MultiPointerGestureDetector) - } - - companion object { - private const val MAX_POINTERS = 2 - - /** Factory method that creates a new instance of MultiPointerGestureDetector */ - fun newInstance(): MultiPointerGestureDetector { - return MultiPointerGestureDetector() - } - } - - private var mGestureInProgress = false - private var mPointerCount = 0 - private var mNewPointerCount = 0 - private val mId = IntArray(MAX_POINTERS) { MotionEvent.INVALID_POINTER_ID } - private val mStartX = FloatArray(MAX_POINTERS) - private val mStartY = FloatArray(MAX_POINTERS) - private val mCurrentX = FloatArray(MAX_POINTERS) - private val mCurrentY = FloatArray(MAX_POINTERS) - - private var mListener: Listener? = null - - init { - reset() - } - - /** - * Sets the listener. - * - * @param listener listener to set - */ - fun setListener(listener: Listener?) { - mListener = listener - } - - /** Resets the component to the initial state. */ - fun reset() { - mGestureInProgress = false - mPointerCount = 0 - for (i in 0 until MAX_POINTERS) { - mId[i] = MotionEvent.INVALID_POINTER_ID - } - } - - /** - * This method can be overridden in order to perform threshold check or something similar. - * - * @return whether or not to start a new gesture - */ - protected open fun shouldStartGesture(): Boolean { - return true - } - - /** Starts a new gesture and calls the listener just before starting it. */ - private fun startGesture() { - if (!mGestureInProgress) { - mListener?.onGestureBegin(this) - mGestureInProgress = true - } - } - - /** Stops the current gesture and calls the listener right after stopping it. */ - private fun stopGesture() { - if (mGestureInProgress) { - mGestureInProgress = false - mListener?.onGestureEnd(this) - } - } - - /** - * Gets the index of the i-th pressed pointer. Normally, the index will be equal to i, except in - * the case when the pointer is released. - * - * @return index of the specified pointer or -1 if not found (i.e. not enough pointers are down) - */ - private fun getPressedPointerIndex(event: MotionEvent, i: Int): Int { - val count = event.pointerCount - val action = event.actionMasked - val index = event.actionIndex - var adjustedIndex = i - - if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { - if (adjustedIndex >= index) { - adjustedIndex++ - } - } - return if (adjustedIndex < count) adjustedIndex else -1 - } - - /** Gets the number of pressed pointers (fingers down). */ - private fun getPressedPointerCount(event: MotionEvent): Int { - var count = event.pointerCount - val action = event.actionMasked - if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { - count-- - } - return count - } - - private fun updatePointersOnTap(event: MotionEvent) { - mPointerCount = 0 - for (i in 0 until MAX_POINTERS) { - val index = getPressedPointerIndex(event, i) - if (index == -1) { - mId[i] = MotionEvent.INVALID_POINTER_ID - } else { - mId[i] = event.getPointerId(index) - mCurrentX[i] = event.getX(index) - mStartX[i] = mCurrentX[i] - mCurrentY[i] = event.getY(index) - mStartY[i] = mCurrentY[i] - mPointerCount++ - } - } - } - - private fun updatePointersOnMove(event: MotionEvent) { - for (i in 0 until MAX_POINTERS) { - val index = event.findPointerIndex(mId[i]) - if (index != -1) { - mCurrentX[i] = event.getX(index) - mCurrentY[i] = event.getY(index) - } - } - } - - /** - * Handles the given motion event. - * - * @param event event to handle - * @return whether or not the event was handled - */ - fun onTouchEvent(event: MotionEvent): Boolean { - when (event.actionMasked) { - MotionEvent.ACTION_MOVE -> { - // update pointers - updatePointersOnMove(event) - // start a new gesture if not already started - if (!mGestureInProgress && mPointerCount > 0 && shouldStartGesture()) { - startGesture() - } - // notify listener - if (mGestureInProgress) { - mListener?.onGestureUpdate(this) - } - } - - MotionEvent.ACTION_DOWN, - MotionEvent.ACTION_POINTER_DOWN, - MotionEvent.ACTION_POINTER_UP, - MotionEvent.ACTION_UP -> { - // restart gesture whenever the number of pointers changes - mNewPointerCount = getPressedPointerCount(event) - stopGesture() - updatePointersOnTap(event) - if (mPointerCount > 0 && shouldStartGesture()) { - startGesture() - } - } - - MotionEvent.ACTION_CANCEL -> { - mNewPointerCount = 0 - stopGesture() - reset() - } - } - return true - } - - /** Restarts the current gesture (if any). */ - fun restartGesture() { - if (!mGestureInProgress) { - return - } - stopGesture() - for (i in 0 until MAX_POINTERS) { - mStartX[i] = mCurrentX[i] - mStartY[i] = mCurrentY[i] - } - startGesture() - } - - /** Gets whether there is a gesture in progress */ - fun isGestureInProgress(): Boolean { - return mGestureInProgress - } - - /** Gets the number of pointers after the current gesture */ - fun getNewPointerCount(): Int { - return mNewPointerCount - } - - /** Gets the number of pointers in the current gesture */ - fun getPointerCount(): Int { - return mPointerCount - } - - /** - * Gets the start X coordinates for all pointers Mutable array is exposed for performance - * reasons and is not to be modified by the callers. - */ - fun getStartX(): FloatArray { - return mStartX - } - - /** - * Gets the start Y coordinates for all pointers Mutable array is exposed for performance - * reasons and is not to be modified by the callers. - */ - fun getStartY(): FloatArray { - return mStartY - } - - /** - * Gets the current X coordinates for all pointers Mutable array is exposed for performance - * reasons and is not to be modified by the callers. - */ - fun getCurrentX(): FloatArray { - return mCurrentX - } - - /** - * Gets the current Y coordinates for all pointers Mutable array is exposed for performance - * reasons and is not to be modified by the callers. - */ - fun getCurrentY(): FloatArray { - return mCurrentY - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/gestures/TransformGestureDetector.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/gestures/TransformGestureDetector.kt deleted file mode 100644 index 9dd0b5813f0..00000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/gestures/TransformGestureDetector.kt +++ /dev/null @@ -1,154 +0,0 @@ -package fr.free.nrw.commons.media.zoomControllers.gestures - -import android.view.MotionEvent -import kotlin.math.atan2 -import kotlin.math.hypot - -/** - * Component that detects translation, scale and rotation based on touch events. - * - * This class notifies its listeners whenever a gesture begins, updates or ends. The instance of - * this detector is passed to the listeners, so it can be queried for pivot, translation, scale or - * rotation. - */ -class TransformGestureDetector(private val mDetector: MultiPointerGestureDetector) : - MultiPointerGestureDetector.Listener { - - /** The listener for receiving notifications when gestures occur. */ - interface Listener { - /** A callback called right before the gesture is about to start. */ - fun onGestureBegin(detector: TransformGestureDetector) - - /** A callback called each time the gesture gets updated. */ - fun onGestureUpdate(detector: TransformGestureDetector) - - /** A callback called right after the gesture has finished. */ - fun onGestureEnd(detector: TransformGestureDetector) - } - - private var mListener: Listener? = null - - init { - mDetector.setListener(this) - } - - /** Factory method that creates a new instance of TransformGestureDetector */ - companion object { - fun newInstance(): TransformGestureDetector { - return TransformGestureDetector(MultiPointerGestureDetector.newInstance()) - } - } - - /** - * Sets the listener. - * - * @param listener listener to set - */ - fun setListener(listener: Listener?) { - mListener = listener - } - - /** Resets the component to the initial state. */ - fun reset() { - mDetector.reset() - } - - /** - * Handles the given motion event. - * - * @param event event to handle - * @return whether or not the event was handled - */ - fun onTouchEvent(event: MotionEvent): Boolean { - return mDetector.onTouchEvent(event) - } - - override fun onGestureBegin(detector: MultiPointerGestureDetector) { - mListener?.onGestureBegin(this) - } - - override fun onGestureUpdate(detector: MultiPointerGestureDetector) { - mListener?.onGestureUpdate(this) - } - - override fun onGestureEnd(detector: MultiPointerGestureDetector) { - mListener?.onGestureEnd(this) - } - - private fun calcAverage(arr: FloatArray, len: Int): Float { - val sum = arr.take(len).sum() - return if (len > 0) sum / len else 0f - } - - /** Restarts the current gesture (if any). */ - fun restartGesture() { - mDetector.restartGesture() - } - - /** Gets whether there is a gesture in progress */ - fun isGestureInProgress(): Boolean { - return mDetector.isGestureInProgress() - } - - /** Gets the number of pointers after the current gesture */ - fun getNewPointerCount(): Int { - return mDetector.getNewPointerCount() - } - - /** Gets the number of pointers in the current gesture */ - fun getPointerCount(): Int { - return mDetector.getPointerCount() - } - - /** Gets the X coordinate of the pivot point */ - fun getPivotX(): Float { - return calcAverage(mDetector.getStartX(), mDetector.getPointerCount()) - } - - /** Gets the Y coordinate of the pivot point */ - fun getPivotY(): Float { - return calcAverage(mDetector.getStartY(), mDetector.getPointerCount()) - } - - /** Gets the X component of the translation */ - fun getTranslationX(): Float { - return calcAverage(mDetector.getCurrentX(), mDetector.getPointerCount()) - - calcAverage(mDetector.getStartX(), mDetector.getPointerCount()) - } - - /** Gets the Y component of the translation */ - fun getTranslationY(): Float { - return calcAverage(mDetector.getCurrentY(), mDetector.getPointerCount()) - - calcAverage(mDetector.getStartY(), mDetector.getPointerCount()) - } - - /** Gets the scale */ - fun getScale(): Float { - return if (mDetector.getPointerCount() < 2) { - 1f - } else { - val startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0] - val startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0] - val currentDeltaX = mDetector.getCurrentX()[1] - mDetector.getCurrentX()[0] - val currentDeltaY = mDetector.getCurrentY()[1] - mDetector.getCurrentY()[0] - val startDist = hypot(startDeltaX, startDeltaY) - val currentDist = hypot(currentDeltaX, currentDeltaY) - currentDist / startDist - } - } - - /** Gets the rotation in radians */ - fun getRotation(): Float { - return if (mDetector.getPointerCount() < 2) { - 0f - } else { - val startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0] - val startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0] - val currentDeltaX = mDetector.getCurrentX()[1] - mDetector.getCurrentX()[0] - val currentDeltaY = mDetector.getCurrentY()[1] - mDetector.getCurrentY()[0] - val startAngle = atan2(startDeltaY, startDeltaX) - val currentAngle = atan2(currentDeltaY, currentDeltaX) - currentAngle - startAngle - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/AbstractAnimatedZoomableController.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/AbstractAnimatedZoomableController.kt deleted file mode 100644 index 27aa7fe2f6d..00000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/AbstractAnimatedZoomableController.kt +++ /dev/null @@ -1,147 +0,0 @@ -package fr.free.nrw.commons.media.zoomControllers.zoomable - -import android.graphics.Matrix -import android.graphics.PointF -import com.facebook.common.logging.FLog -import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector - -/** - * Abstract class for ZoomableController that adds animation capabilities to - * DefaultZoomableController. - */ -abstract class AbstractAnimatedZoomableController( - transformGestureDetector: TransformGestureDetector -) : DefaultZoomableController(transformGestureDetector) { - - private var isAnimating: Boolean = false - private val startValues = FloatArray(9) - private val stopValues = FloatArray(9) - private val currentValues = FloatArray(9) - private val newTransform = Matrix() - private val workingTransform = Matrix() - - override fun reset() { - FLog.v(logTag, "reset") - stopAnimation() - workingTransform.reset() - newTransform.reset() - super.reset() - } - - /** Returns true if the zoomable transform is identity matrix, and the controller is idle. */ - override fun isIdentity(): Boolean { - return !isAnimating && super.isIdentity() - } - - /** - * Zooms to the desired scale and positions the image so that the given image point corresponds - * to the given view point. - * - * If this method is called while an animation or gesture is already in progress, the current - * animation or gesture will be stopped first. - * - * @param scale desired scale, will be limited to {min, max} scale factor - * @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1) - * @param viewPoint 2D point in view's absolute coordinate system - */ - override fun zoomToPoint(scale: Float, imagePoint: PointF, viewPoint: PointF) { - zoomToPoint(scale, imagePoint, viewPoint, LIMIT_ALL, 0, null) - } - - /** - * Zooms to the desired scale and positions the image so that the given image point corresponds - * to the given view point. - * - * If this method is called while an animation or gesture is already in progress, the current - * animation or gesture will be stopped first. - * - * @param scale desired scale, will be limited to {min, max} scale factor - * @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1) - * @param viewPoint 2D point in view's absolute coordinate system - * @param limitFlags whether to limit translation and/or scale. - * @param durationMs length of animation of the zoom, or 0 if no animation desired - * @param onAnimationComplete code to run when the animation completes. Ignored if durationMs=0 - */ - fun zoomToPoint( - scale: Float, - imagePoint: PointF, - viewPoint: PointF, - @LimitFlag limitFlags: Int, - durationMs: Long, - onAnimationComplete: Runnable? - ) { - FLog.v(logTag, "zoomToPoint: duration $durationMs ms") - calculateZoomToPointTransform(newTransform, scale, imagePoint, viewPoint, limitFlags) - setTransform(newTransform, durationMs, onAnimationComplete) - } - - /** - * Sets a new zoomable transformation and animates to it if desired. - * - * If this method is called while an animation or gesture is already in progress, the current - * animation or gesture will be stopped first. - * - * @param newTransform new transform to make active - * @param durationMs duration of the animation, or 0 to not animate - * @param onAnimationComplete code to run when the animation completes. Ignored if durationMs=0 - */ - private fun setTransform( - newTransform: Matrix, - durationMs: Long, - onAnimationComplete: Runnable? - ) { - FLog.v(logTag, "setTransform: duration $durationMs ms") - if (durationMs <= 0) { - setTransformImmediate(newTransform) - } else { - setTransformAnimated(newTransform, durationMs, onAnimationComplete) - } - } - - private fun setTransformImmediate(newTransform: Matrix) { - FLog.v(logTag, "setTransformImmediate") - stopAnimation() - workingTransform.set(newTransform) - super.setTransform(newTransform) - getDetector().restartGesture() - } - - protected fun getIsAnimating(): Boolean = isAnimating - - protected fun setAnimating(isAnimating: Boolean) { - this.isAnimating = isAnimating - } - - protected fun getStartValues(): FloatArray = startValues - - protected fun getStopValues(): FloatArray = stopValues - - protected fun getWorkingTransform(): Matrix = workingTransform - - override fun onGestureBegin(detector: TransformGestureDetector) { - FLog.v(logTag, "onGestureBegin") - stopAnimation() - super.onGestureBegin(detector) - } - - override fun onGestureUpdate(detector: TransformGestureDetector) { - FLog.v(logTag, "onGestureUpdate ${if (isAnimating) "(ignored)" else ""}") - if (isAnimating) return - super.onGestureUpdate(detector) - } - - protected fun calculateInterpolation(outMatrix: Matrix, fraction: Float) { - for (i in 0..8) { - currentValues[i] = (1 - fraction) * startValues[i] + fraction * stopValues[i] - } - outMatrix.setValues(currentValues) - } - - abstract fun setTransformAnimated( - newTransform: Matrix, durationMs: Long, onAnimationComplete: Runnable? - ) - - protected abstract fun stopAnimation() - - protected abstract val logTag: Class<*> -} diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/AnimatedZoomableController.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/AnimatedZoomableController.kt deleted file mode 100644 index 6ba9270078c..00000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/AnimatedZoomableController.kt +++ /dev/null @@ -1,75 +0,0 @@ -package fr.free.nrw.commons.media.zoomControllers.zoomable - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.animation.ValueAnimator -import android.annotation.SuppressLint -import android.graphics.Matrix -import android.view.animation.DecelerateInterpolator -import com.facebook.common.logging.FLog -import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector - -/** - * ZoomableController that adds animation capabilities to DefaultZoomableController using standard - * Android animation classes - */ -class AnimatedZoomableController private constructor() : - AbstractAnimatedZoomableController(TransformGestureDetector.newInstance()) { - - private val valueAnimator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f).apply { - interpolator = DecelerateInterpolator() - } - - companion object { - fun newInstance(): AnimatedZoomableController { - return AnimatedZoomableController() - } - } - - @SuppressLint("NewApi") - override fun setTransformAnimated( - newTransform: Matrix, durationMs: Long, onAnimationComplete: Runnable? - ) { - FLog.v(logTag, "setTransformAnimated: duration $durationMs ms") - stopAnimation() - require(durationMs > 0) { "Duration must be greater than zero" } - check(!getIsAnimating()) { "Animation is already in progress" } - setAnimating(true) - valueAnimator.duration = durationMs - getTransform().getValues(getStartValues()) - newTransform.getValues(getStopValues()) - valueAnimator.addUpdateListener { animator -> - calculateInterpolation(getWorkingTransform(), animator.animatedValue as Float) - super.setTransform(getWorkingTransform()) - } - valueAnimator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationCancel(animation: Animator) { - FLog.v(logTag, "setTransformAnimated: animation cancelled") - onAnimationStopped() - } - - override fun onAnimationEnd(animation: Animator) { - FLog.v(logTag, "setTransformAnimated: animation finished") - onAnimationStopped() - } - - private fun onAnimationStopped() { - onAnimationComplete?.run() - setAnimating(false) - getDetector().restartGesture() - } - }) - valueAnimator.start() - } - - @SuppressLint("NewApi") - override fun stopAnimation() { - if (!getIsAnimating()) return - FLog.v(logTag, "stopAnimation") - valueAnimator.cancel() - valueAnimator.removeAllUpdateListeners() - valueAnimator.removeAllListeners() - } - - override val logTag: Class<*> = AnimatedZoomableController::class.java -} diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/DefaultZoomableController.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/DefaultZoomableController.kt deleted file mode 100644 index 3d6498f6a2d..00000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/DefaultZoomableController.kt +++ /dev/null @@ -1,607 +0,0 @@ -package fr.free.nrw.commons.media.zoomControllers.zoomable - -import android.graphics.Matrix -import android.graphics.PointF -import android.graphics.RectF -import android.view.MotionEvent -import androidx.annotation.IntDef -import com.facebook.common.logging.FLog -import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector -import kotlin.math.abs - -/** Zoomable controller that calculates transformation based on touch events. */ -open class DefaultZoomableController( - private val mGestureDetector: TransformGestureDetector -) : ZoomableController, TransformGestureDetector.Listener { - - /** Interface for handling call backs when the image bounds are set. */ - fun interface ImageBoundsListener { - fun onImageBoundsSet(imageBounds: RectF) - } - - @IntDef( - flag = true, - value = [LIMIT_NONE, LIMIT_TRANSLATION_X, LIMIT_TRANSLATION_Y, LIMIT_SCALE, LIMIT_ALL] - ) - @Retention - annotation class LimitFlag - - companion object { - const val LIMIT_NONE = 0 - const val LIMIT_TRANSLATION_X = 1 - const val LIMIT_TRANSLATION_Y = 2 - const val LIMIT_SCALE = 4 - const val LIMIT_ALL = LIMIT_TRANSLATION_X or LIMIT_TRANSLATION_Y or LIMIT_SCALE - - private const val EPS = 1e-3f - - private val TAG: Class<*> = DefaultZoomableController::class.java - - private val IDENTITY_RECT = RectF(0f, 0f, 1f, 1f) - - fun newInstance(): DefaultZoomableController { - return DefaultZoomableController(TransformGestureDetector.newInstance()) - } - } - - private var mImageBoundsListener: ImageBoundsListener? = null - - private var mListener: ZoomableController.Listener? = null - - private var mIsEnabled = false - private var mIsRotationEnabled = false - private var mIsScaleEnabled = true - private var mIsTranslationEnabled = true - private var mIsGestureZoomEnabled = true - - private var mMinScaleFactor = 1.0f - private var mMaxScaleFactor = 2.0f - - // View bounds, in view-absolute coordinates - private val mViewBounds = RectF() - // Non-transformed image bounds, in view-absolute coordinates - private val mImageBounds = RectF() - // Transformed image bounds, in view-absolute coordinates - private val mTransformedImageBounds = RectF() - - private val mPreviousTransform = Matrix() - private val mActiveTransform = Matrix() - private val mActiveTransformInverse = Matrix() - private val mTempValues = FloatArray(9) - private val mTempRect = RectF() - private var mWasTransformCorrected = false - - init { - mGestureDetector.setListener(this) - } - - /** Rests the controller. */ - open fun reset() { - FLog.v(TAG, "reset") - mGestureDetector.reset() - mPreviousTransform.reset() - mActiveTransform.reset() - onTransformChanged() - } - - /** Sets the zoomable listener. */ - override fun setListener(listener: ZoomableController.Listener?) { - mListener = listener - } - - /** Sets whether the controller is enabled or not. */ - override fun setEnabled(enabled: Boolean) { - mIsEnabled = enabled - if (!enabled) { - reset() - } - } - - /** Gets whether the controller is enabled or not. */ - override fun isEnabled(): Boolean { - return mIsEnabled - } - - /** Sets whether the rotation gesture is enabled or not. */ - fun setRotationEnabled(enabled: Boolean) { - mIsRotationEnabled = enabled - } - - /** Gets whether the rotation gesture is enabled or not. */ - fun isRotationEnabled(): Boolean { - return mIsRotationEnabled - } - - /** Sets whether the scale gesture is enabled or not. */ - fun setScaleEnabled(enabled: Boolean) { - mIsScaleEnabled = enabled - } - - /** Gets whether the scale gesture is enabled or not. */ - fun isScaleEnabled(): Boolean { - return mIsScaleEnabled - } - - /** Sets whether the translation gesture is enabled or not. */ - fun setTranslationEnabled(enabled: Boolean) { - mIsTranslationEnabled = enabled - } - - /** Gets whether the translations gesture is enabled or not. */ - fun isTranslationEnabled(): Boolean { - return mIsTranslationEnabled - } - - /** - * Sets the minimum scale factor allowed. - * - *

Hierarchy's scaling (if any) is not taken into account. - */ - fun setMinScaleFactor(minScaleFactor: Float) { - mMinScaleFactor = minScaleFactor - } - - /** Gets the minimum scale factor allowed. */ - fun getMinScaleFactor(): Float { - return mMinScaleFactor - } - - /** - * Sets the maximum scale factor allowed. - * - *

Hierarchy's scaling (if any) is not taken into account. - */ - fun setMaxScaleFactor(maxScaleFactor: Float) { - mMaxScaleFactor = maxScaleFactor - } - - /** Gets the maximum scale factor allowed. */ - fun getMaxScaleFactor(): Float { - return mMaxScaleFactor - } - - /** Sets whether gesture zooms are enabled or not. */ - fun setGestureZoomEnabled(isGestureZoomEnabled: Boolean) { - mIsGestureZoomEnabled = isGestureZoomEnabled - } - - /** Gets whether gesture zooms are enabled or not. */ - fun isGestureZoomEnabled(): Boolean { - return mIsGestureZoomEnabled - } - - /** Gets the current scale factor. */ - override fun getScaleFactor(): Float { - return getMatrixScaleFactor(mActiveTransform) - } - - /** Sets the image bounds, in view-absolute coordinates. */ - override fun setImageBounds(imageBounds: RectF) { - if (imageBounds != mImageBounds) { - mImageBounds.set(imageBounds) - onTransformChanged() - mImageBoundsListener?.onImageBoundsSet(mImageBounds) - } - } - - /** Gets the non-transformed image bounds, in view-absolute coordinates. */ - fun getImageBounds(): RectF { - return mImageBounds - } - - /** Gets the transformed image bounds, in view-absolute coordinates */ - private fun getTransformedImageBounds(): RectF { - return mTransformedImageBounds - } - - /** Sets the view bounds. */ - override fun setViewBounds(viewBounds: RectF) { - mViewBounds.set(viewBounds) - } - - /** Gets the view bounds. */ - fun getViewBounds(): RectF { - return mViewBounds - } - - /** Sets the image bounds listener. */ - fun setImageBoundsListener(imageBoundsListener: ImageBoundsListener?) { - mImageBoundsListener = imageBoundsListener - } - - /** Gets the image bounds listener. */ - fun getImageBoundsListener(): ImageBoundsListener? { - return mImageBoundsListener - } - - /** Returns true if the zoomable transform is identity matrix. */ - override fun isIdentity(): Boolean { - return isMatrixIdentity(mActiveTransform, 1e-3f) - } - - /** - * Returns true if the transform was corrected during the last update. - * - *

We should rename this method to `wasTransformedWithoutCorrection` and just return the - * internal flag directly. However, this requires interface change and negation of meaning. - */ - override fun wasTransformCorrected(): Boolean { - return mWasTransformCorrected - } - - /** - * Gets the matrix that transforms image-absolute coordinates to view-absolute coordinates. The - * zoomable transformation is taken into account. - * - *

Internal matrix is exposed for performance reasons and is not to be modified by the - * callers. - */ - override fun getTransform(): Matrix { - return mActiveTransform - } - - /** - * Gets the matrix that transforms image-relative coordinates to view-absolute coordinates. The - * zoomable transformation is taken into account. - */ - fun getImageRelativeToViewAbsoluteTransform(outMatrix: Matrix) { - outMatrix.setRectToRect(IDENTITY_RECT, mTransformedImageBounds, Matrix.ScaleToFit.FILL) - } - - /** - * Maps point from view-absolute to image-relative coordinates. This takes into account the - * zoomable transformation. - */ - fun mapViewToImage(viewPoint: PointF): PointF { - val points = mTempValues - points[0] = viewPoint.x - points[1] = viewPoint.y - mActiveTransform.invert(mActiveTransformInverse) - mActiveTransformInverse.mapPoints(points, 0, points, 0, 1) - mapAbsoluteToRelative(points, points, 1) - return PointF(points[0], points[1]) - } - - /** - * Maps point from image-relative to view-absolute coordinates. This takes into account the - * zoomable transformation. - */ - fun mapImageToView(imagePoint: PointF): PointF { - val points = mTempValues - points[0] = imagePoint.x - points[1] = imagePoint.y - mapRelativeToAbsolute(points, points, 1) - mActiveTransform.mapPoints(points, 0, points, 0, 1) - return PointF(points[0], points[1]) - } - - /** - * Maps array of 2D points from view-absolute to image-relative coordinates. This does NOT take - * into account the zoomable transformation. Points are represented by a float array of [x0, y0, - * x1, y1, ...]. - * - * @param destPoints destination array (may be the same as source array) - * @param srcPoints source array - * @param numPoints number of points to map - */ - private fun mapAbsoluteToRelative( - destPoints: FloatArray, - srcPoints: FloatArray, - numPoints: Int - ) { - for (i in 0 until numPoints) { - destPoints[i * 2] = (srcPoints[i * 2] - mImageBounds.left) / mImageBounds.width() - destPoints[i * 2 + 1] = - (srcPoints[i * 2 + 1] - mImageBounds.top) / mImageBounds.height() - } - } - - /** - * Maps array of 2D points from image-relative to view-absolute coordinates. This does NOT take - * into account the zoomable transformation. Points are represented by float array of - * [x0, y0, x1, y1, ...]. - * - * @param destPoints destination array (may be the same as source array) - * @param srcPoints source array - * @param numPoints number of points to map - */ - private fun mapRelativeToAbsolute( - destPoints: FloatArray, - srcPoints: FloatArray, - numPoints: Int - ) { - for (i in 0 until numPoints) { - destPoints[i * 2] = srcPoints[i * 2] * mImageBounds.width() + mImageBounds.left - destPoints[i * 2 + 1] = srcPoints[i * 2 + 1] * mImageBounds.height() + mImageBounds.top - } - } - - /** - * Zooms to the desired scale and positions the image so that the given image point - * corresponds to the given view point. - * - * @param scale desired scale, will be limited to {min, max} scale factor - * @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1) - * @param viewPoint 2D point in view's absolute coordinate system - */ - open fun zoomToPoint(scale: Float, imagePoint: PointF, viewPoint: PointF) { - FLog.v(TAG, "zoomToPoint") - calculateZoomToPointTransform(mActiveTransform, scale, imagePoint, viewPoint, LIMIT_ALL) - onTransformChanged() - } - - /** - * Calculates the zoom transformation that would zoom to the desired scale and position - * the image so that the given image point corresponds to the given view point. - * - * @param outTransform the matrix to store the result to - * @param scale desired scale, will be limited to {min, max} scale factor - * @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1) - * @param viewPoint 2D point in view's absolute coordinate system - * @param limitFlags whether to limit translation and/or scale. - * @return whether or not the transform has been corrected due to limitation - */ - protected fun calculateZoomToPointTransform( - outTransform: Matrix, - scale: Float, - imagePoint: PointF, - viewPoint: PointF, - @LimitFlag limitFlags: Int - ): Boolean { - val viewAbsolute = mTempValues - viewAbsolute[0] = imagePoint.x - viewAbsolute[1] = imagePoint.y - mapRelativeToAbsolute(viewAbsolute, viewAbsolute, 1) - val distanceX = viewPoint.x - viewAbsolute[0] - val distanceY = viewPoint.y - viewAbsolute[1] - var transformCorrected = false - outTransform.setScale(scale, scale, viewAbsolute[0], viewAbsolute[1]) - transformCorrected = transformCorrected or - limitScale(outTransform, viewAbsolute[0], viewAbsolute[1], limitFlags) - outTransform.postTranslate(distanceX, distanceY) - transformCorrected = transformCorrected or limitTranslation(outTransform, limitFlags) - return transformCorrected - } - - /** Sets a new zoom transformation. */ - fun setTransform(newTransform: Matrix) { - FLog.v(TAG, "setTransform") - mActiveTransform.set(newTransform) - onTransformChanged() - } - - /** Gets the gesture detector. */ - protected fun getDetector(): TransformGestureDetector { - return mGestureDetector - } - - /** Notifies controller of the received touch event. */ - override fun onTouchEvent(event: MotionEvent): Boolean { - FLog.v(TAG, "onTouchEvent: action: ", event.action) - return if (mIsEnabled && mIsGestureZoomEnabled) { - mGestureDetector.onTouchEvent(event) - } else { - false - } - } - - /* TransformGestureDetector.Listener methods */ - - override fun onGestureBegin(detector: TransformGestureDetector) { - FLog.v(TAG, "onGestureBegin") - mPreviousTransform.set(mActiveTransform) - onTransformBegin() - mWasTransformCorrected = !canScrollInAllDirection() - } - - override fun onGestureUpdate(detector: TransformGestureDetector) { - FLog.v(TAG, "onGestureUpdate") - val transformCorrected = calculateGestureTransform(mActiveTransform, LIMIT_ALL) - onTransformChanged() - if (transformCorrected) { - mGestureDetector.restartGesture() - } - mWasTransformCorrected = transformCorrected - } - - override fun onGestureEnd(detector: TransformGestureDetector) { - FLog.v(TAG, "onGestureEnd") - onTransformEnd() - } - - /** - * Calculates the zoom transformation based on the current gesture. - * - * @param outTransform the matrix to store the result to - * @param limitTypes whether to limit translation and/or scale. - * @return whether or not the transform has been corrected due to limitation - */ - private fun calculateGestureTransform( - outTransform: Matrix, - @LimitFlag limitTypes: Int - ): Boolean { - val detector = mGestureDetector - var transformCorrected = false - outTransform.set(mPreviousTransform) - if (mIsRotationEnabled) { - val angle = detector.getRotation() * (180 / Math.PI).toFloat() - outTransform.postRotate(angle, detector.getPivotX(), detector.getPivotY()) - } - if (mIsScaleEnabled) { - val scale = detector.getScale() - outTransform.postScale(scale, scale, detector.getPivotX(), detector.getPivotY()) - } - transformCorrected = transformCorrected or limitScale( - outTransform, - detector.getPivotX(), - detector.getPivotY(), - limitTypes - ) - if (mIsTranslationEnabled) { - outTransform.postTranslate(detector.getTranslationX(), detector.getTranslationY()) - } - transformCorrected = transformCorrected or limitTranslation(outTransform, limitTypes) - return transformCorrected - } - - private fun onTransformBegin() { - if (mListener != null && isEnabled()) { - mListener?.onTransformBegin(mActiveTransform) - } - } - - private fun onTransformChanged() { - mActiveTransform.mapRect(mTransformedImageBounds, mImageBounds) - if (mListener != null && isEnabled()) { - mListener?.onTransformChanged(mActiveTransform) - } - } - - private fun onTransformEnd() { - if (mListener != null && isEnabled()) { - mListener?.onTransformEnd(mActiveTransform) - } - } - - /** - * Keeps the scaling factor within the specified limits. - * - * @param pivotX x coordinate of the pivot point - * @param pivotY y coordinate of the pivot point - * @param limitTypes whether to limit scale. - * @return whether limiting has been applied or not - */ - private fun limitScale( - transform: Matrix, pivotX: Float, pivotY: Float, @LimitFlag limitTypes: Int - ): Boolean { - if (!shouldLimit(limitTypes, LIMIT_SCALE)) { - return false - } - val currentScale = getMatrixScaleFactor(transform) - val targetScale = limit(currentScale, mMinScaleFactor, mMaxScaleFactor) - return if (targetScale != currentScale) { - val scale = targetScale / currentScale - transform.postScale(scale, scale, pivotX, pivotY) - true - } else { - false - } - } - - /** - * Limits the translation so that there are no empty spaces on the sides if possible. - */ - private fun limitTranslation(transform: Matrix, @LimitFlag limitTypes: Int): Boolean { - if (!shouldLimit(limitTypes, LIMIT_TRANSLATION_X or LIMIT_TRANSLATION_Y)) { - return false - } - val b = mTempRect - b.set(mImageBounds) - transform.mapRect(b) - val offsetLeft = - if (shouldLimit(limitTypes, LIMIT_TRANSLATION_X)) getOffset( - b.left, b.right, mViewBounds.left, mViewBounds.right, mImageBounds.centerX() - ) else 0f - val offsetTop = - if (shouldLimit(limitTypes, LIMIT_TRANSLATION_Y)) getOffset( - b.top, b.bottom, mViewBounds.top, mViewBounds.bottom, mImageBounds.centerY() - ) else 0f - - return if (offsetLeft != 0f || offsetTop != 0f) { - transform.postTranslate(offsetLeft, offsetTop) - true - } else { - false - } - } - - /** - * Checks whether the specified limit flag is present in the limits provided. - */ - private fun shouldLimit(@LimitFlag limits: Int, @LimitFlag flag: Int): Boolean { - return (limits and flag) != LIMIT_NONE - } - - /** - * Returns the offset necessary to make sure that: - * - The image is centered if it's smaller than the limit - * - There is no empty space if the image is bigger than the limit - */ - private fun getOffset( - imageStart: Float, imageEnd: Float, limitStart: Float, limitEnd: Float, limitCenter: Float - ): Float { - val imageWidth = imageEnd - imageStart - val limitWidth = limitEnd - limitStart - val limitInnerWidth = minOf(limitCenter - limitStart, limitEnd - limitCenter) * 2 - - return when { - imageWidth < limitInnerWidth -> limitCenter - (imageEnd + imageStart) / 2 - imageWidth < limitWidth -> if (limitCenter < (limitStart + limitEnd) / 2) { - limitStart - imageStart - } else { - limitEnd - imageEnd - } - imageStart > limitStart -> limitStart - imageStart - imageEnd < limitEnd -> limitEnd - imageEnd - else -> 0f - } - } - - /** Limits the value to the given min and max range. */ - private fun limit(value: Float, min: Float, max: Float): Float { - return min.coerceAtLeast(value).coerceAtMost(max) - } - - /** - * Gets the scale factor for the given matrix. Assumes equal scaling for X and Y axis. - */ - private fun getMatrixScaleFactor(transform: Matrix): Float { - transform.getValues(mTempValues) - return mTempValues[Matrix.MSCALE_X] - } - - /** Checks if the matrix is an identity matrix within a given tolerance `eps`. */ - private fun isMatrixIdentity(transform: Matrix, eps: Float): Boolean { - transform.getValues(mTempValues) - mTempValues[0] -= 1.0f // m00 - mTempValues[4] -= 1.0f // m11 - mTempValues[8] -= 1.0f // m22 - return mTempValues.all { abs(it) <= eps } - } - - /** Returns whether the scroll can happen in all directions. */ - private fun canScrollInAllDirection(): Boolean { - return mTransformedImageBounds.left < mViewBounds.left - EPS && - mTransformedImageBounds.top < mViewBounds.top - EPS && - mTransformedImageBounds.right > mViewBounds.right + EPS && - mTransformedImageBounds.bottom > mViewBounds.bottom + EPS - } - - override fun computeHorizontalScrollRange(): Int { - return mTransformedImageBounds.width().toInt() - } - - override fun computeHorizontalScrollOffset(): Int { - return (mViewBounds.left - mTransformedImageBounds.left).toInt() - } - - override fun computeHorizontalScrollExtent(): Int { - return mViewBounds.width().toInt() - } - - override fun computeVerticalScrollRange(): Int { - return mTransformedImageBounds.height().toInt() - } - - override fun computeVerticalScrollOffset(): Int { - return (mViewBounds.top - mTransformedImageBounds.top).toInt() - } - - override fun computeVerticalScrollExtent(): Int { - return mViewBounds.height().toInt() - } - - fun getListener(): ZoomableController.Listener? { - return mListener - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/DoubleTapGestureListener.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/DoubleTapGestureListener.kt deleted file mode 100644 index 5f9a256e650..00000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/DoubleTapGestureListener.kt +++ /dev/null @@ -1,96 +0,0 @@ -package fr.free.nrw.commons.media.zoomControllers.zoomable - -import android.graphics.PointF -import android.view.GestureDetector -import android.view.MotionEvent -import kotlin.math.abs -import kotlin.math.hypot - -/** - * Tap gesture listener for double tap to zoom/unzoom and double-tap-and-drag to zoom. - * - * @see ZoomableDraweeView.setTapListener - */ -class DoubleTapGestureListener( - private val draweeView: ZoomableDraweeView, - private val onSingleTap: (() -> Unit)? = null -) : GestureDetector.SimpleOnGestureListener() { - - companion object { - private const val DURATION_MS = 300L - private const val DOUBLE_TAP_SCROLL_THRESHOLD = 20 - } - - private val doubleTapViewPoint = PointF() - private val doubleTapImagePoint = PointF() - private var doubleTapScale = 1f - private var doubleTapScroll = false - - override fun onSingleTapConfirmed(e: MotionEvent): Boolean { - if(onSingleTap != null) { - onSingleTap.invoke() - return true - } else { - return super.onSingleTapConfirmed(e) - } - } - - override fun onDoubleTapEvent(e: MotionEvent): Boolean { - val zc = draweeView.getZoomableController() as AbstractAnimatedZoomableController - val vp = PointF(e.x, e.y) - val ip = zc.mapViewToImage(vp) - - when (e.actionMasked) { - MotionEvent.ACTION_DOWN -> { - doubleTapViewPoint.set(vp) - doubleTapImagePoint.set(ip) - doubleTapScale = zc.getScaleFactor() - } - - MotionEvent.ACTION_MOVE -> { - doubleTapScroll = doubleTapScroll || shouldStartDoubleTapScroll(vp) - if (doubleTapScroll) { - val scale = calcScale(vp) - zc.zoomToPoint(scale, doubleTapImagePoint, doubleTapViewPoint) - } - } - - MotionEvent.ACTION_UP -> { - if (doubleTapScroll) { - val scale = calcScale(vp) - zc.zoomToPoint(scale, doubleTapImagePoint, doubleTapViewPoint) - } else { - val maxScale = zc.getMaxScaleFactor() - val minScale = zc.getMinScaleFactor() - val targetScale = - if (zc.getScaleFactor() < (maxScale + minScale) / 2) maxScale else minScale - - zc.zoomToPoint( - targetScale, - ip, - vp, - DefaultZoomableController.LIMIT_ALL, - DURATION_MS, - null - ) - } - doubleTapScroll = false - } - } - return true - } - - private fun shouldStartDoubleTapScroll(viewPoint: PointF): Boolean { - val dist = hypot( - (viewPoint.x - doubleTapViewPoint.x).toDouble(), - (viewPoint.y - doubleTapViewPoint.y).toDouble() - ) - return dist > DOUBLE_TAP_SCROLL_THRESHOLD - } - - private fun calcScale(currentViewPoint: PointF): Float { - val dy = currentViewPoint.y - doubleTapViewPoint.y - val t = 1 + abs(dy) * 0.001f - return if (dy < 0) doubleTapScale / t else doubleTapScale * t - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/GestureListenerWrapper.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/GestureListenerWrapper.kt deleted file mode 100644 index 994e98cabf7..00000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/GestureListenerWrapper.kt +++ /dev/null @@ -1,61 +0,0 @@ -package fr.free.nrw.commons.media.zoomControllers.zoomable - -import android.view.GestureDetector -import android.view.MotionEvent - -/** Wrapper for SimpleOnGestureListener as GestureDetector does not allow changing its listener. */ -class GestureListenerWrapper : GestureDetector.SimpleOnGestureListener() { - - private var delegate: GestureDetector.SimpleOnGestureListener = - GestureDetector.SimpleOnGestureListener() - - fun setListener(listener: GestureDetector.SimpleOnGestureListener) { - delegate = listener - } - - override fun onLongPress(e: MotionEvent) { - delegate.onLongPress(e) - } - - override fun onScroll( - e1: MotionEvent?, - e2: MotionEvent, - distanceX: Float, - distanceY: Float - ): Boolean { - return delegate.onScroll(e1, e2, distanceX, distanceY) - } - - override fun onFling( - e1: MotionEvent?, - e2: MotionEvent, - velocityX: Float, - velocityY: Float - ): Boolean { - return delegate.onFling(e1, e2, velocityX, velocityY) - } - - override fun onShowPress(e: MotionEvent) { - delegate.onShowPress(e) - } - - override fun onDown(e: MotionEvent): Boolean { - return delegate.onDown(e) - } - - override fun onDoubleTap(e: MotionEvent): Boolean { - return delegate.onDoubleTap(e) - } - - override fun onDoubleTapEvent(e: MotionEvent): Boolean { - return delegate.onDoubleTapEvent(e) - } - - override fun onSingleTapConfirmed(e: MotionEvent): Boolean { - return delegate.onSingleTapConfirmed(e) - } - - override fun onSingleTapUp(e: MotionEvent): Boolean { - return delegate.onSingleTapUp(e) - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/MultiGestureListener.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/MultiGestureListener.kt deleted file mode 100644 index 70f7921c12e..00000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/MultiGestureListener.kt +++ /dev/null @@ -1,150 +0,0 @@ -package fr.free.nrw.commons.media.zoomControllers.zoomable - -import android.os.Build -import android.view.GestureDetector -import android.view.MotionEvent -import androidx.annotation.RequiresApi -import java.util.Collections.synchronizedList - -/** - * Gesture listener that allows multiple child listeners to be added and notified about gesture - * events. - * - * NOTE: The order of the listeners is important. Listeners can consume gesture events. For - * example, if one of the child listeners consumes [onLongPress] (the listener returned true), - * subsequent listeners will not be notified about the event anymore since it has been consumed. - */ -class MultiGestureListener : GestureDetector.SimpleOnGestureListener() { - - private val listeners: MutableList = - synchronizedList(mutableListOf()) - - /** - * Adds a listener to the multi-gesture listener. - * - * NOTE: The order of the listeners is important since gesture events can be consumed. - * - * @param listener the listener to be added - */ - @Synchronized - fun addListener(listener: GestureDetector.SimpleOnGestureListener) { - listeners.add(listener) - } - - /** - * Removes the given listener so that it will not be notified about future events. - * - * NOTE: The order of the listeners is important since gesture events can be consumed. - * - * @param listener the listener to remove - */ - @Synchronized - fun removeListener(listener: GestureDetector.SimpleOnGestureListener) { - listeners.remove(listener) - } - - @Synchronized - override fun onSingleTapUp(e: MotionEvent): Boolean { - for (listener in listeners) { - if (listener.onSingleTapUp(e)) { - return true - } - } - return false - } - - @Synchronized - override fun onLongPress(e: MotionEvent) { - for (listener in listeners) { - listener.onLongPress(e) - } - } - - @Synchronized - override fun onScroll( - e1: MotionEvent?, - e2: MotionEvent, - distanceX: Float, - distanceY: Float - ): Boolean { - for (listener in listeners) { - if (listener.onScroll(e1, e2, distanceX, distanceY)) { - return true - } - } - return false - } - - @Synchronized - override fun onFling( - e1: MotionEvent?, - e2: MotionEvent, - velocityX: Float, - velocityY: Float - ): Boolean { - for (listener in listeners) { - if (listener.onFling(e1, e2, velocityX, velocityY)) { - return true - } - } - return false - } - - @Synchronized - override fun onShowPress(e: MotionEvent) { - for (listener in listeners) { - listener.onShowPress(e) - } - } - - @Synchronized - override fun onDown(e: MotionEvent): Boolean { - for (listener in listeners) { - if (listener.onDown(e)) { - return true - } - } - return false - } - - @Synchronized - override fun onDoubleTap(e: MotionEvent): Boolean { - for (listener in listeners) { - if (listener.onDoubleTap(e)) { - return true - } - } - return false - } - - @Synchronized - override fun onDoubleTapEvent(e: MotionEvent): Boolean { - for (listener in listeners) { - if (listener.onDoubleTapEvent(e)) { - return true - } - } - return false - } - - @Synchronized - override fun onSingleTapConfirmed(e: MotionEvent): Boolean { - for (listener in listeners) { - if (listener.onSingleTapConfirmed(e)) { - return true - } - } - return false - } - - @RequiresApi(Build.VERSION_CODES.M) - @Synchronized - override fun onContextClick(e: MotionEvent): Boolean { - for (listener in listeners) { - if (listener.onContextClick(e)) { - return true - } - } - return false - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/MultiZoomableControllerListener.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/MultiZoomableControllerListener.kt deleted file mode 100644 index 3bd7c00b957..00000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/MultiZoomableControllerListener.kt +++ /dev/null @@ -1,46 +0,0 @@ -package fr.free.nrw.commons.media.zoomControllers.zoomable - -import android.graphics.Matrix -import java.util.ArrayList - -/** - * MultiZoomableControllerListener that allows multiple listeners to be added and notified about - * transform events. - * - * NOTE: The order of the listeners is important. Listeners can consume transform events. - */ -class MultiZoomableControllerListener : ZoomableController.Listener { - - private val listeners: MutableList = mutableListOf() - - @Synchronized - override fun onTransformBegin(transform: Matrix) { - for (listener in listeners) { - listener.onTransformBegin(transform) - } - } - - @Synchronized - override fun onTransformChanged(transform: Matrix) { - for (listener in listeners) { - listener.onTransformChanged(transform) - } - } - - @Synchronized - override fun onTransformEnd(transform: Matrix) { - for (listener in listeners) { - listener.onTransformEnd(transform) - } - } - - @Synchronized - fun addListener(listener: ZoomableController.Listener) { - listeners.add(listener) - } - - @Synchronized - fun removeListener(listener: ZoomableController.Listener) { - listeners.remove(listener) - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/ZoomableController.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/ZoomableController.kt deleted file mode 100644 index 503f6716967..00000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/ZoomableController.kt +++ /dev/null @@ -1,120 +0,0 @@ -package fr.free.nrw.commons.media.zoomControllers.zoomable - -import android.graphics.Matrix -import android.graphics.RectF -import android.view.MotionEvent - -/** - * Interface for implementing a controller that works with [ZoomableDraweeView] to control the - * zoom. - */ -interface ZoomableController { - - /** Listener interface. */ - interface Listener { - - /** - * Notifies the view that the transform began. - * - * @param transform the current transform matrix - */ - fun onTransformBegin(transform: Matrix) - - /** - * Notifies the view that the transform changed. - * - * @param transform the new matrix - */ - fun onTransformChanged(transform: Matrix) - - /** - * Notifies the view that the transform ended. - * - * @param transform the current transform matrix - */ - fun onTransformEnd(transform: Matrix) - } - - /** - * Enables the controller. The controller is enabled when the image has been loaded. - * - * @param enabled whether to enable the controller - */ - fun setEnabled(enabled: Boolean) - - /** - * Gets whether the controller is enabled. This should return the last value passed - * to [setEnabled]. - * @return whether the controller is enabled. - */ - fun isEnabled(): Boolean - - /** - * Sets the listener for the controller to call back when the matrix changes. - * - * @param listener the listener - */ - fun setListener(listener: Listener?) - - /** - * Gets the current scale factor. A convenience method for calculating the scale from the - * transform. - * - * @return the current scale factor - */ - fun getScaleFactor(): Float - - /** Returns true if the zoomable transform is identity matrix, and the controller is idle. */ - fun isIdentity(): Boolean - - /** - * Returns true if the transform was corrected during the last update. - * - *

This mainly happens when a gesture would cause the image to get out of limits and the - * transform gets corrected in order to prevent that. - */ - fun wasTransformCorrected(): Boolean - - /** See [androidx.core.view.ScrollingView]. */ - fun computeHorizontalScrollRange(): Int - - fun computeHorizontalScrollOffset(): Int - - fun computeHorizontalScrollExtent(): Int - - fun computeVerticalScrollRange(): Int - - fun computeVerticalScrollOffset(): Int - - fun computeVerticalScrollExtent(): Int - - /** - * Gets the current transform. - * - * @return the transform - */ - fun getTransform(): Matrix - - /** - * Sets the bounds of the image post transform prior to application of the zoomable - * transformation. - * - * @param imageBounds the bounds of the image - */ - fun setImageBounds(imageBounds: RectF) - - /** - * Sets the bounds of the view. - * - * @param viewBounds the bounds of the view - */ - fun setViewBounds(viewBounds: RectF) - - /** - * Allows the controller to handle a touch event. - * - * @param event the touch event - * @return whether the controller handled the event - */ - fun onTouchEvent(event: MotionEvent): Boolean -} diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/ZoomableDraweeView.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/ZoomableDraweeView.kt deleted file mode 100644 index 5df63668b74..00000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/ZoomableDraweeView.kt +++ /dev/null @@ -1,320 +0,0 @@ -package fr.free.nrw.commons.media.zoomControllers.zoomable - -import android.annotation.SuppressLint -import android.content.Context -import android.content.res.Resources -import android.graphics.Canvas -import android.graphics.Matrix -import android.graphics.RectF -import android.graphics.drawable.Animatable -import android.util.AttributeSet -import android.view.GestureDetector -import android.view.MotionEvent - -import androidx.core.view.ScrollingView -import com.facebook.common.internal.Preconditions -import com.facebook.common.logging.FLog -import com.facebook.drawee.controller.AbstractDraweeController -import com.facebook.drawee.controller.BaseControllerListener -import com.facebook.drawee.drawable.ScalingUtils -import com.facebook.drawee.generic.GenericDraweeHierarchy -import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder -import com.facebook.drawee.generic.GenericDraweeHierarchyInflater -import com.facebook.drawee.interfaces.DraweeController -import com.facebook.drawee.view.DraweeView - -/** - * DraweeView that has zoomable capabilities. - * - *

Once the image loads, pinch-to-zoom and translation gestures are enabled. - */ -open class ZoomableDraweeView : DraweeView, ScrollingView { - - companion object { - private val TAG = ZoomableDraweeView::class.java - private const val HUGE_IMAGE_SCALE_FACTOR_THRESHOLD = 1.1f - } - - private val imageBounds = RectF() - private val viewBounds = RectF() - - private var hugeImageController: DraweeController? = null - private var zoomableController: ZoomableController = createZoomableController() - private var tapGestureDetector: GestureDetector? = null - private var allowTouchInterceptionWhileZoomed = true - private var isDialToneEnabled = false - private var zoomingEnabled = true - private var transformationListener: TransformationListener? = null - - private val controllerListener = object : BaseControllerListener() { - override fun onFinalImageSet(id: String, imageInfo: Any?, animatable: Animatable?) { - this@ZoomableDraweeView.onFinalImageSet() - } - - override fun onRelease(id: String) { - this@ZoomableDraweeView.onRelease() - } - } - - private val zoomableListener = object : ZoomableController.Listener { - override fun onTransformBegin(transform: Matrix) {} - - override fun onTransformChanged(transform: Matrix) { - this@ZoomableDraweeView.onTransformChanged(transform) - } - - override fun onTransformEnd(transform: Matrix) { - transformationListener?.onTransformationEnd() - } - } - - private val tapListenerWrapper = GestureListenerWrapper() - - constructor(context: Context, hierarchy: GenericDraweeHierarchy) : super(context) { - setHierarchy(hierarchy) - init() - } - - constructor(context: Context) : super(context) { - inflateHierarchy(context, null) - init() - } - - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { - inflateHierarchy(context, attrs) - init() - } - - constructor(context: Context, attrs: AttributeSet?, defStyle: Int) - : super(context, attrs, defStyle) { - inflateHierarchy(context, attrs) - init() - } - - fun setTransformationListener(transformationListener: TransformationListener) { - this.transformationListener = transformationListener - } - - protected fun inflateHierarchy(context: Context, attrs: AttributeSet?) { - val resources: Resources = context.resources - val builder = GenericDraweeHierarchyBuilder(resources) - .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER) - GenericDraweeHierarchyInflater.updateBuilder(builder, context, attrs) - aspectRatio = builder.desiredAspectRatio - setHierarchy(builder.build()) - } - - private fun init() { - zoomableController.setListener(zoomableListener) - tapGestureDetector = GestureDetector(context, tapListenerWrapper) - } - - fun setIsDialToneEnabled(isDialtoneEnabled: Boolean) { - this.isDialToneEnabled = isDialtoneEnabled - } - - protected fun getImageBounds(outBounds: RectF) { - hierarchy.getActualImageBounds(outBounds) - } - - protected fun getLimitBounds(outBounds: RectF) { - outBounds.set(0f, 0f, width.toFloat(), height.toFloat()) - } - - fun setZoomableController(zoomableController: ZoomableController) { - Preconditions.checkNotNull(zoomableController) - this.zoomableController.setListener(null) - this.zoomableController = zoomableController - this.zoomableController.setListener(zoomableListener) - } - - fun getZoomableController(): ZoomableController = zoomableController - - fun allowsTouchInterceptionWhileZoomed(): Boolean = allowTouchInterceptionWhileZoomed - - fun setAllowTouchInterceptionWhileZoomed(allow: Boolean) { - allowTouchInterceptionWhileZoomed = allow - } - - fun setTapListener(tapListener: GestureDetector.SimpleOnGestureListener) { - tapListenerWrapper.setListener(tapListener) - } - - fun setIsLongpressEnabled(enabled: Boolean) { - tapGestureDetector?.setIsLongpressEnabled(enabled) - } - - fun setZoomingEnabled(zoomingEnabled: Boolean) { - this.zoomingEnabled = zoomingEnabled - zoomableController.setEnabled(false) - } - - override fun setController(controller: DraweeController?) { - setControllers(controller, null) - } - - fun setControllers(controller: DraweeController?, hugeImageController: DraweeController?) { - setControllersInternal(null, null) - zoomableController.setEnabled(false) - setControllersInternal(controller, hugeImageController) - } - - private fun setControllersInternal( - controller: DraweeController?, - hugeImageController: DraweeController? - ) { - removeControllerListener(getController()) - addControllerListener(controller) - this.hugeImageController = hugeImageController - super.setController(controller) - } - - private fun maybeSetHugeImageController() { - if ( - hugeImageController != null - && - zoomableController.getScaleFactor() > HUGE_IMAGE_SCALE_FACTOR_THRESHOLD - ) { - setControllersInternal(hugeImageController, null) - } - } - - private fun removeControllerListener(controller: DraweeController?) { - if (controller is AbstractDraweeController<*, *>) { - controller.removeControllerListener(controllerListener) - } - } - - private fun addControllerListener(controller: DraweeController?) { - if (controller is AbstractDraweeController<*, *>) { - controller.addControllerListener(controllerListener) - } - } - - override fun onDraw(canvas: Canvas) { - val saveCount = canvas.save() - canvas.concat(zoomableController.getTransform()) - try { - super.onDraw(canvas) - } catch (e: Exception) { - val controller = controller - if (controller is AbstractDraweeController<*, *>) { - val callerContext = controller.callerContext - if (callerContext != null) { - throw RuntimeException("Exception in onDraw, callerContext=${callerContext}", e) - } - } - throw e - } - canvas.restoreToCount(saveCount) - } - - @SuppressLint("ClickableViewAccessibility") - override fun onTouchEvent(event: MotionEvent): Boolean { - var action = event.actionMasked - FLog.v(getLogTag(), "onTouchEvent: $action, view ${hashCode()}, received") - - if (!isDialToneEnabled && tapGestureDetector?.onTouchEvent(event) == true) { - FLog.v( - getLogTag(), - "onTouchEvent: $action, view ${hashCode()}, handled by tap gesture detector" - ) - return true - } - - if (!isDialToneEnabled && zoomableController.onTouchEvent(event)) { - FLog.v( - getLogTag(), - "onTouchEvent: $action, view ${hashCode()}, handled by zoomable controller" - ) - if (!allowTouchInterceptionWhileZoomed && !zoomableController.isIdentity()) { - parent.requestDisallowInterceptTouchEvent(true) - } - return true - } - - if (super.onTouchEvent(event)) { - FLog.v( - getLogTag(), - "onTouchEvent: $action, view ${hashCode()}, handled by the super" - ) - return true - } - - // If none of our components handled the event, we send a cancel event to avoid unwanted actions. - val cancelEvent = MotionEvent.obtain(event).apply { action = MotionEvent.ACTION_CANCEL } - tapGestureDetector?.onTouchEvent(cancelEvent) - zoomableController.onTouchEvent(cancelEvent) - cancelEvent.recycle() - - return false - } - - override fun computeHorizontalScrollRange(): Int = - zoomableController.computeHorizontalScrollRange() - - override fun computeHorizontalScrollOffset(): Int = - zoomableController.computeHorizontalScrollOffset() - - override fun computeHorizontalScrollExtent(): Int = - zoomableController.computeHorizontalScrollExtent() - - override fun computeVerticalScrollRange(): Int = - zoomableController.computeVerticalScrollRange() - - override fun computeVerticalScrollOffset(): Int = - zoomableController.computeVerticalScrollOffset() - - override fun computeVerticalScrollExtent(): Int = - zoomableController.computeVerticalScrollExtent() - - - override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { - FLog.v(getLogTag(), "onLayout: view ${hashCode()}") - super.onLayout(changed, left, top, right, bottom) - updateZoomableControllerBounds() - } - - private fun onFinalImageSet() { - FLog.v(getLogTag(), "onFinalImageSet: view ${hashCode()}") - if (!zoomableController.isEnabled() && zoomingEnabled) { - zoomableController.setEnabled(true) - updateZoomableControllerBounds() - } - } - - private fun onRelease() { - FLog.v(getLogTag(), "onRelease: view ${hashCode()}") - zoomableController.setEnabled(false) - } - - protected fun onTransformChanged(transform: Matrix) { - FLog.v(getLogTag(), "onTransformChanged: view ${hashCode()}, transform: $transform") - maybeSetHugeImageController() - invalidate() - } - - protected fun updateZoomableControllerBounds() { - getImageBounds(imageBounds) - getLimitBounds(viewBounds) - zoomableController.setImageBounds(imageBounds) - zoomableController.setViewBounds(viewBounds) - - FLog.v( - getLogTag(), - "updateZoomableControllerBounds: view ${hashCode()}, " + - "view bounds: $viewBounds, image bounds: $imageBounds" - ) - } - - protected fun getLogTag(): Class<*> = TAG - - protected fun createZoomableController(): ZoomableController = AnimatedZoomableController.newInstance() - - /** - * Interface to listen for scale change events. - */ - interface TransformationListener { - fun onTransformationEnd() - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt index 61dace3bb69..4fa9aed214b 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt @@ -45,10 +45,10 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager -import com.bumptech.glide.Glide -import com.bumptech.glide.load.engine.GlideException -import com.bumptech.glide.request.RequestListener -import com.bumptech.glide.request.target.Target +import coil3.load +import coil3.request.placeholder +import coil3.request.error +import coil3.dispose import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback import com.google.android.material.snackbar.Snackbar @@ -2531,8 +2531,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), } selectedPlace?.pic?.substringAfterLast("/")?.takeIf { it.isNotEmpty() }?.let { imageName -> - Glide.with(binding!!.bottomSheetDetails.icon.context) - .clear(binding!!.bottomSheetDetails.icon) + binding!!.bottomSheetDetails.icon.dispose() val loadingDrawable = ContextCompat.getDrawable( binding!!.bottomSheetDetails.icon.context, @@ -2543,33 +2542,18 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), R.anim.rotate ) - Glide.with(binding!!.bottomSheetDetails.icon.context) - .load("https://commons.wikimedia.org/wiki/Special:Redirect/file/$imageName?width=25") - .placeholder(loadingDrawable) - .error(selectedPlace!!.label.icon) - .listener(object : RequestListener { - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target, - isFirstResource: Boolean - ): Boolean { + binding!!.bottomSheetDetails.icon.load("https://commons.wikimedia.org/wiki/Special:Redirect/file/$imageName?width=25") { + placeholder(loadingDrawable) + error(selectedPlace!!.label.icon) + listener( + onSuccess = { _, _ -> binding!!.bottomSheetDetails.icon.clearAnimation() - return false - } - - override fun onResourceReady( - resource: Drawable, - model: Any, - target: Target?, - dataSource: com.bumptech.glide.load.DataSource, - isFirstResource: Boolean - ): Boolean { + }, + onError = { _, _ -> binding!!.bottomSheetDetails.icon.clearAnimation() - return false } - }) - .into(binding!!.bottomSheetDetails.icon) + ) + } if (binding!!.bottomSheetDetails.icon.drawable != null && binding!!.bottomSheetDetails.icon.drawable.constantState == loadingDrawable?.constantState) { binding!!.bottomSheetDetails.icon.startAnimation(animation) diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt index 99de68f809b..4539a95ea69 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt @@ -1,14 +1,16 @@ package fr.free.nrw.commons.profile.leaderboard import android.app.Activity -import android.net.Uri import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ImageView import android.widget.TextView import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.RecyclerView -import com.facebook.drawee.view.SimpleDraweeView +import coil3.load +import coil3.request.transformations +import coil3.transform.CircleCropTransformation import fr.free.nrw.commons.R import fr.free.nrw.commons.profile.ProfileActivity import fr.free.nrw.commons.profile.leaderboard.LeaderboardList.Companion.DIFF_CALLBACK @@ -21,7 +23,7 @@ import fr.free.nrw.commons.profile.leaderboard.LeaderboardListAdapter.ListViewHo class LeaderboardListAdapter : PagedListAdapter(DIFF_CALLBACK) { inner class ListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { var rank: TextView? = itemView.findViewById(R.id.user_rank) - var avatar: SimpleDraweeView? = itemView.findViewById(R.id.user_avatar) + var avatar: ImageView? = itemView.findViewById(R.id.user_avatar) var username: TextView? = itemView.findViewById(R.id.user_name) var count: TextView? = itemView.findViewById(R.id.user_count) } @@ -47,7 +49,9 @@ class LeaderboardListAdapter : PagedListAdapter val item = getItem(position)!! rank?.text = item.rank.toString() - avatar?.setImageURI(Uri.parse(item.avatar)) + avatar?.load(item.avatar) { + transformations(CircleCropTransformation()) + } username?.text = item.username count?.text = item.categoryCount.toString() diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.kt index 34fd5ab5815..28390b5772d 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.kt @@ -1,14 +1,16 @@ package fr.free.nrw.commons.profile.leaderboard import android.accounts.AccountManager -import android.net.Uri import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ImageView import android.widget.TextView import android.widget.Toast import androidx.recyclerview.widget.RecyclerView -import com.facebook.drawee.view.SimpleDraweeView +import coil3.load +import coil3.request.transformations +import coil3.transform.CircleCropTransformation import fr.free.nrw.commons.BuildConfig import fr.free.nrw.commons.R import fr.free.nrw.commons.profile.leaderboard.UserDetailAdapter.DataViewHolder @@ -26,7 +28,7 @@ class UserDetailAdapter(private val leaderboardResponse: LeaderboardResponse) : class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val rank: TextView = itemView.findViewById(R.id.rank) - val avatar: SimpleDraweeView = itemView.findViewById(R.id.avatar) + val avatar: ImageView = itemView.findViewById(R.id.avatar) val username: TextView = itemView.findViewById(R.id.username) val count: TextView = itemView.findViewById(R.id.count) } @@ -53,7 +55,9 @@ class UserDetailAdapter(private val leaderboardResponse: LeaderboardResponse) : override fun onBindViewHolder(holder: DataViewHolder, position: Int) = with(holder) { val resources = itemView.context.resources - avatar.setImageURI(Uri.parse(leaderboardResponse.avatar)) + avatar.load(leaderboardResponse.avatar) { + transformations(CircleCropTransformation()) + } username.text = leaderboardResponse.username rank.text = String.format( Locale.getDefault(), diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt b/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt index 11fd1e6a631..be10cef0aff 100644 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt @@ -8,10 +8,8 @@ import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat - -import com.facebook.drawee.drawable.ProgressBarDrawable -import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder +import coil3.load +import coil3.request.error import fr.free.nrw.commons.databinding.ActivityQuizBinding import java.util.ArrayList @@ -94,13 +92,9 @@ class QuizActivity : AppCompatActivity() { binding.question.questionText.text = quiz[questionIndex].question binding.questionTitle.text = getString(R.string.question) + quiz[questionIndex].questionNumber - binding.question.questionImage.hierarchy = GenericDraweeHierarchyBuilder - .newInstance(resources) - .setFailureImage(VectorDrawableCompat.create(resources, R.drawable.ic_error_outline_black_24dp, theme)) - .setProgressBarImage(ProgressBarDrawable()) - .build() - - binding.question.questionImage.setImageURI(quiz[questionIndex].getUrl()) + binding.question.questionImage.load(quiz[questionIndex].getUrl()) { + error(R.drawable.ic_error_outline_black_24dp) + } isPositiveAnswerChecked = false isNegativeAnswerChecked = false diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt index dccb77af1eb..1d028f4365e 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt @@ -1,6 +1,9 @@ package fr.free.nrw.commons.review import android.annotation.SuppressLint +import coil3.load +import coil3.request.placeholder +import coil3.request.error import android.content.Context import android.content.Intent import android.graphics.PorterDuff @@ -188,7 +191,10 @@ class ReviewActivity : BaseActivity() { return } - binding.reviewImageView.setImageURI(media.thumbUrl) + binding.reviewImageView.load(media.thumbUrl) { + placeholder(R.drawable.image_placeholder) + error(R.drawable.image_placeholder) + } reviewController.onImageRefreshed(media) // filename is updated compositeDisposable.add( diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsAdapter.kt b/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsAdapter.kt index 5b3eb51400a..7061cd85c3d 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsAdapter.kt @@ -3,23 +3,18 @@ package fr.free.nrw.commons.upload import android.content.ClipData import android.content.ClipboardManager import android.content.Context.CLIPBOARD_SERVICE -import android.net.Uri -import android.text.TextUtils import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.webkit.URLUtil import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import com.facebook.imagepipeline.request.ImageRequest import com.google.android.material.snackbar.Snackbar import fr.free.nrw.commons.R import fr.free.nrw.commons.contributions.Contribution -import java.io.File /** * Adapter for displaying failed uploads in a paginated list in FailedUploadsFragment. This adapter @@ -76,21 +71,10 @@ class FailedUploadsAdapter( if (item != null) { holder.titleTextView.setText(item.media.displayTitle) } - var imageRequest: ImageRequest? = null - val imageSource: String = item?.localUri.toString() - - if (!TextUtils.isEmpty(imageSource)) { - if (URLUtil.isFileUrl(imageSource)) { - imageRequest = ImageRequest.fromUri(Uri.parse(imageSource))!! - } else if (imageSource != null) { - val file = File(imageSource) - imageRequest = ImageRequest.fromFile(file)!! - } - - if (imageRequest != null) { - holder.itemImage.setImageRequest(imageRequest) - } - } + holder.itemImage.loadUploadItemImage( + item?.localUri?.toString(), + R.drawable.ic_image_black_24dp + ) if (item != null) { if (item.state == Contribution.STATE_FAILED) { @@ -109,7 +93,6 @@ class FailedUploadsAdapter( holder.retryButton.setOnClickListener { callback.restartUpload(position) } - holder.itemImage.setImageRequest(imageRequest) } /** @@ -118,7 +101,7 @@ class FailedUploadsAdapter( class ViewHolder( itemView: View, ) : RecyclerView.ViewHolder(itemView) { - var itemImage: com.facebook.drawee.view.SimpleDraweeView = + var itemImage: ImageView = itemView.findViewById(R.id.itemImage) var titleTextView: TextView = itemView.findViewById(R.id.titleTextView) var itemProgress: ProgressBar = itemView.findViewById(R.id.itemProgress) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsAdapter.kt b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsAdapter.kt index 7f0b8aba19a..d9ab73d4117 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsAdapter.kt @@ -3,23 +3,18 @@ package fr.free.nrw.commons.upload import android.content.ClipData import android.content.ClipboardManager import android.content.Context.CLIPBOARD_SERVICE -import android.net.Uri -import android.text.TextUtils import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.webkit.URLUtil import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import com.facebook.imagepipeline.request.ImageRequest import com.google.android.material.snackbar.Snackbar import fr.free.nrw.commons.R import fr.free.nrw.commons.contributions.Contribution -import java.io.File /** * Adapter for displaying pending uploads in a paginated list in PendingUploadsFragment. This adapter @@ -94,7 +89,7 @@ class PendingUploadsAdapter( class ViewHolder( itemView: View, ) : RecyclerView.ViewHolder(itemView) { - var itemImage: com.facebook.drawee.view.SimpleDraweeView = + var itemImage: ImageView = itemView.findViewById(R.id.itemImage) var titleTextView: TextView = itemView.findViewById(R.id.titleTextView) var itemProgress: ProgressBar = itemView.findViewById(R.id.itemProgress) @@ -120,21 +115,10 @@ class PendingUploadsAdapter( true } - val imageSource: String = contribution.localUri.toString() - var imageRequest: ImageRequest? = null - - if (!TextUtils.isEmpty(imageSource)) { - if (URLUtil.isFileUrl(imageSource)) { - imageRequest = ImageRequest.fromUri(Uri.parse(imageSource)) - } else { - val file = File(imageSource) - imageRequest = ImageRequest.fromFile(file) - } - } - - if (imageRequest != null) { - itemImage.setImageRequest(imageRequest) - } + itemImage.loadUploadItemImage( + contribution.localUri?.toString(), + R.drawable.ic_image_black_24dp + ) bindState(contribution.state) bindProgress(contribution.transferred, contribution.dataLength, contribution.state) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.kt index c5d82ed1076..486e408ad22 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.kt @@ -2,15 +2,15 @@ package fr.free.nrw.commons.upload import android.app.Dialog import android.content.DialogInterface -import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.Window import androidx.fragment.app.DialogFragment -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat -import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder +import coil3.load +import coil3.request.placeholder +import coil3.request.error import fr.free.nrw.commons.R import fr.free.nrw.commons.databinding.FragmentSimilarImageDialogBinding import java.io.File @@ -38,35 +38,15 @@ class SimilarImageDialogFragment : DialogFragment() { ): View { _binding = FragmentSimilarImageDialogBinding.inflate(inflater, container, false) - binding.orginalImage.hierarchy = - GenericDraweeHierarchyBuilder.newInstance(resources).setPlaceholderImage( - VectorDrawableCompat.create( - resources, R.drawable.ic_image_black_24dp, requireContext().theme - ) - ).setFailureImage( - VectorDrawableCompat.create( - resources, R.drawable.ic_error_outline_black_24dp, requireContext().theme - ) - ).build() - - binding.possibleImage.hierarchy = - GenericDraweeHierarchyBuilder.newInstance(resources).setPlaceholderImage( - VectorDrawableCompat.create( - resources, R.drawable.ic_image_black_24dp, requireContext().theme - ) - ).setFailureImage( - VectorDrawableCompat.create( - resources, R.drawable.ic_error_outline_black_24dp, requireContext().theme - ) - ).build() - arguments?.let { - binding.orginalImage.setImageURI( - Uri.fromFile(File(it.getString("originalImagePath")!!)) - ) - binding.possibleImage.setImageURI( - Uri.fromFile(File(it.getString("possibleImagePath")!!)) - ) + binding.orginalImage.load(File(it.getString("originalImagePath")!!)) { + placeholder(R.drawable.ic_image_black_24dp) + error(R.drawable.ic_error_outline_black_24dp) + } + binding.possibleImage.load(File(it.getString("possibleImagePath")!!)) { + placeholder(R.drawable.ic_image_black_24dp) + error(R.drawable.ic_error_outline_black_24dp) + } } binding.postiveButton.setOnClickListener { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailsAdapter.kt b/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailsAdapter.kt index d467f9bf63c..fd0813c21d3 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailsAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailsAdapter.kt @@ -1,14 +1,13 @@ package fr.free.nrw.commons.upload import android.graphics.drawable.GradientDrawable -import android.net.Uri import android.view.LayoutInflater import android.view.ViewGroup import android.widget.ImageView import android.widget.RelativeLayout import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView -import com.facebook.drawee.view.SimpleDraweeView +import coil3.load import fr.free.nrw.commons.R import fr.free.nrw.commons.databinding.ItemUploadThumbnailBinding import fr.free.nrw.commons.filepicker.UploadableFile @@ -40,7 +39,7 @@ internal class ThumbnailsAdapter(private val callback: Callback) : inner class ViewHolder(val binding: ItemUploadThumbnailBinding) : RecyclerView.ViewHolder(binding.root) { private val rlContainer: RelativeLayout = binding.rlContainer - private val background: SimpleDraweeView = binding.ivThumbnail + private val background: ImageView = binding.ivThumbnail private val ivError: ImageView = binding.ivError private val ivCross: ImageView = binding.icCross @@ -50,7 +49,7 @@ internal class ThumbnailsAdapter(private val callback: Callback) : fun bind(position: Int) { val uploadableFile = uploadableFiles[position] val uri = uploadableFile.getMediaUri() - background.setImageURI(Uri.fromFile(File(uri.toString()))) + background.load(File(uri.toString())) if (position == callback.getCurrentSelectedFilePosition()) { val border = GradientDrawable() border.shape = GradientDrawable.RECTANGLE diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadItemImageLoader.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadItemImageLoader.kt new file mode 100644 index 00000000000..293accdbc6d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadItemImageLoader.kt @@ -0,0 +1,184 @@ +package fr.free.nrw.commons.upload + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.webkit.URLUtil +import android.widget.ImageView +import coil3.load +import coil3.request.error +import coil3.request.placeholder +import timber.log.Timber +import java.io.File +import kotlin.math.max + +internal fun ImageView.loadUploadItemImage( + imageSource: String?, + placeholderResId: Int, +) { + val data: Any? = + imageSource + ?.takeUnless { it.isBlank() } + ?.let { + val uri = Uri.parse(it) + when { + URLUtil.isHttpUrl(it) || URLUtil.isHttpsUrl(it) -> it + !uri.scheme.isNullOrBlank() -> uri + else -> File(it) + } + } + + try { + load(data) { + placeholder(placeholderResId) + error(placeholderResId) + listener( + onError = { _, result -> + if (!tryLoadDownsampledImage(imageSource, placeholderResId, result.throwable)) { + Timber.e(result.throwable, "Unable to load upload item image: %s", imageSource) + } + } + ) + } + } catch (error: Exception) { + Timber.e(error, "Unable to start upload item image load: %s", imageSource) + setImageResource(placeholderResId) + } +} + +private fun ImageView.tryLoadDownsampledImage( + imageSource: String?, + placeholderResId: Int, + error: Throwable?, +): Boolean { + if (imageSource.isNullOrBlank() || !isOutOfMemory(error)) { + return false + } + + return try { + val downsampledBitmap = + decodeDownsampledBitmap( + imageSource, + requestedWidth = measuredWidth.takeIf { it > 0 } ?: width, + requestedHeight = measuredHeight.takeIf { it > 0 } ?: height + ) ?: return false + + load(downsampledBitmap) { + placeholder(placeholderResId) + error(placeholderResId) + listener( + onError = { _, result -> + Timber.e(result.throwable, "Unable to load downsampled upload item image: %s", imageSource) + setImageResource(placeholderResId) + } + ) + } + true + } catch (oom: OutOfMemoryError) { + Timber.e(oom, "Out of memory while downsampling upload item image: %s", imageSource) + setImageResource(placeholderResId) + true + } catch (exception: Exception) { + Timber.e(exception, "Unable to downsample upload item image: %s", imageSource) + setImageResource(placeholderResId) + true + } +} + +private fun ImageView.decodeDownsampledBitmap( + imageSource: String, + requestedWidth: Int, + requestedHeight: Int, +): Bitmap? { + val targetWidth = requestedWidth.takeIf { it > 0 } ?: DEFAULT_DOWNSAMPLED_IMAGE_SIZE_PX + val targetHeight = requestedHeight.takeIf { it > 0 } ?: DEFAULT_DOWNSAMPLED_IMAGE_SIZE_PX + val uri = Uri.parse(imageSource) + + return when { + !uri.scheme.isNullOrBlank() && !URLUtil.isHttpUrl(imageSource) && !URLUtil.isHttpsUrl(imageSource) -> + decodeDownsampledBitmapFromUri(uri, targetWidth, targetHeight) + + else -> decodeDownsampledBitmapFromFile(File(imageSource), targetWidth, targetHeight) + } +} + +private fun ImageView.decodeDownsampledBitmapFromUri( + uri: Uri, + requestedWidth: Int, + requestedHeight: Int, +): Bitmap? { + val bounds = + BitmapFactory.Options().apply { + inJustDecodeBounds = true + } + context.contentResolver.openInputStream(uri)?.use { stream -> + BitmapFactory.decodeStream(stream, null, bounds) + } ?: return null + + val options = + BitmapFactory.Options().apply { + inSampleSize = calculateInSampleSize(bounds, requestedWidth, requestedHeight) + } + return context.contentResolver.openInputStream(uri)?.use { stream -> + BitmapFactory.decodeStream(stream, null, options) + } +} + +private fun decodeDownsampledBitmapFromFile( + file: File, + requestedWidth: Int, + requestedHeight: Int, +): Bitmap? { + if (!file.exists()) { + return null + } + + val bounds = + BitmapFactory.Options().apply { + inJustDecodeBounds = true + } + BitmapFactory.decodeFile(file.path, bounds) + + val options = + BitmapFactory.Options().apply { + inSampleSize = calculateInSampleSize(bounds, requestedWidth, requestedHeight) + } + return BitmapFactory.decodeFile(file.path, options) +} + +private fun calculateInSampleSize( + options: BitmapFactory.Options, + requestedWidth: Int, + requestedHeight: Int, +): Int { + val safeRequestedWidth = max(requestedWidth, 1) + val safeRequestedHeight = max(requestedHeight, 1) + val height = options.outHeight + val width = options.outWidth + var inSampleSize = 1 + + if (height > safeRequestedHeight || width > safeRequestedWidth) { + val halfHeight = height / 2 + val halfWidth = width / 2 + + while (halfHeight / inSampleSize >= safeRequestedHeight && + halfWidth / inSampleSize >= safeRequestedWidth + ) { + inSampleSize *= 2 + } + } + return inSampleSize +} + +private fun isOutOfMemory(error: Throwable?): Boolean { + var current = error + while (current != null) { + if (current is OutOfMemoryError) { + return true + } + current = current.cause + } + return false +} + +private const val DEFAULT_DOWNSAMPLED_IMAGE_SIZE_PX = 1024 diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoryAdapterDelegates.kt b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoryAdapterDelegates.kt index 619db9ab7af..fd8e24256ca 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoryAdapterDelegates.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoryAdapterDelegates.kt @@ -1,6 +1,8 @@ package fr.free.nrw.commons.upload.categories import android.view.View +import coil3.load +import coil3.request.placeholder import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import fr.free.nrw.commons.R import fr.free.nrw.commons.category.CategoryItem @@ -38,9 +40,11 @@ fun uploadCategoryDelegate( } binding.categoryLabel.text = item.name if (item.thumbnail != "null") { - binding.categoryImage.setImageURI(item.thumbnail) + binding.categoryImage.load(item.thumbnail) { + placeholder(R.drawable.commons) + } } else { - binding.categoryImage.setActualImageResource(R.drawable.commons) + binding.categoryImage.setImageResource(R.drawable.commons) } if (item.description != "null") { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/UploadDepictsAdapterDelegates.kt b/app/src/main/java/fr/free/nrw/commons/upload/depicts/UploadDepictsAdapterDelegates.kt index 0f02c9672b6..ad7443d4f40 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/UploadDepictsAdapterDelegates.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/UploadDepictsAdapterDelegates.kt @@ -1,8 +1,9 @@ package fr.free.nrw.commons.upload.depicts -import android.net.Uri import android.text.TextUtils import android.view.View +import coil3.load +import coil3.request.placeholder import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import fr.free.nrw.commons.R import fr.free.nrw.commons.databinding.LayoutUploadDepictsItemBinding @@ -37,9 +38,11 @@ fun uploadDepictsDelegate( binding.description.text = item.description val imageUrl = item.imageUrl if (TextUtils.isEmpty(imageUrl)) { - binding.depictedImage.setActualImageResource(R.drawable.ic_wikidata_logo_24dp) + binding.depictedImage.setImageResource(R.drawable.ic_wikidata_logo_24dp) } else { - binding.depictedImage.setImageURI(Uri.parse(imageUrl)) + binding.depictedImage.load(imageUrl) { + placeholder(R.drawable.ic_wikidata_logo_24dp) + } } } } diff --git a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.kt b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.kt index ab6a45b8573..9582243103d 100644 --- a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.kt +++ b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.kt @@ -5,22 +5,17 @@ import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProvider import android.content.Context import android.content.Intent -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Paint import android.net.Uri import android.os.Build import android.widget.RemoteViews -import androidx.annotation.Nullable -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.core.ImagePipeline -import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber -import com.facebook.imagepipeline.image.CloseableImage -import com.facebook.imagepipeline.request.ImageRequest -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 kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import fr.free.nrw.commons.media.MediaClient import javax.inject.Inject import fr.free.nrw.commons.R @@ -117,7 +112,7 @@ class PicOfDayAppWidget : AppWidgetProvider() { } /** - * Uses Fresco to load an image from Url + * Uses Coil to load an image from Url * @param imageUrl The URL of the image to load. * @param context The application context. * @param views The RemoteViews object used to update the App Widget UI. @@ -131,25 +126,25 @@ class PicOfDayAppWidget : AppWidgetProvider() { appWidgetManager: AppWidgetManager, appWidgetId: Int ) { - val request = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageUrl)).build() - val imagePipeline = Fresco.getImagePipeline() - val dataSource = imagePipeline.fetchDecodedImage(request, context) - - dataSource.subscribe(object : BaseBitmapDataSubscriber() { - override fun onNewResultImpl(tempBitmap: Bitmap?) { - val bitmap = tempBitmap?.let { - Bitmap.createBitmap(it.width, it.height, Bitmap.Config.ARGB_8888).apply { - Canvas(this).drawBitmap(it, 0f, 0f, Paint()) - } + if (imageUrl == null) return + + CoroutineScope(Dispatchers.IO).launch { + 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() + views.setImageViewBitmap(R.id.appwidget_image, bitmap) + appWidgetManager.updateAppWidget(appWidgetId, views) } - views.setImageViewBitmap(R.id.appwidget_image, bitmap) - appWidgetManager.updateAppWidget(appWidgetId, views) + } catch (e: Exception) { + Timber.e(e, "Error loading image for widget") } - - override fun onFailureImpl(dataSource: DataSource>) { - // Ignore failure for now. - } - }, CallerThreadExecutor.getInstance()) + } } override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { diff --git a/app/src/main/res/layout-land/activity_review.xml b/app/src/main/res/layout-land/activity_review.xml index 405ef054c94..204cfe3f1a4 100644 --- a/app/src/main/res/layout-land/activity_review.xml +++ b/app/src/main/res/layout-land/activity_review.xml @@ -87,7 +87,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> - - - - + + + + + + + - - + + android:scaleType="fitCenter" /> + android:scaleType="fitXY" /> - + app:layout_constraintBottom_toBottomOf="parent" /> diff --git a/app/src/main/res/layout/item_failed_upload.xml b/app/src/main/res/layout/item_failed_upload.xml index 73ac9c7b414..4eb9454d7eb 100644 --- a/app/src/main/res/layout/item_failed_upload.xml +++ b/app/src/main/res/layout/item_failed_upload.xml @@ -3,18 +3,16 @@ android:layout_width="match_parent" android:layout_height="wrap_content" xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:fresco="http://schemas.android.com/tools" android:paddingBottom="8dp" android:gravity="center" android:orientation="horizontal"> - + android:scaleType="centerCrop" /> - + app:layout_constraintTop_toTopOf="parent" /> - - + android:scaleType="fitCenter"/> - - + android:scaleType="centerCrop" /> - + app:layout_constraintEnd_toStartOf="@+id/text_container" /> - + app:layout_constraintEnd_toStartOf="@+id/text_container" /> - - diff --git a/app/src/main/res/layout/question_layout.xml b/app/src/main/res/layout/question_layout.xml index 6af04de0823..a30cfc695d7 100644 --- a/app/src/main/res/layout/question_layout.xml +++ b/app/src/main/res/layout/question_layout.xml @@ -5,7 +5,7 @@ android:layout_height="wrap_content" android:gravity="center_horizontal"> - - - @Mock - private lateinit var context: ProducerContext - - @Mock - private lateinit var imageRequest: ImageRequest - - @Mock - private lateinit var uri: Uri - - @Mock - private lateinit var mCacheControl: CacheControl - - @Mock - private lateinit var bytesRange: BytesRange - - @Mock - private lateinit var executor: Executor - - @Mock - private lateinit var call: Call - - @Mock - private lateinit var response: Response - - @Mock - private lateinit var body: ResponseBody - - @Before - @Throws(Exception::class) - fun setUp() { - MockitoAnnotations.openMocks(this) - okHttpClient = OkHttpClient() - fetcher = CustomOkHttpNetworkFetcher(okHttpClient, defaultKvStore) - whenever(context.imageRequest).thenReturn(imageRequest) - whenever(imageRequest.sourceUri).thenReturn(uri) - whenever(imageRequest.bytesRange).thenReturn(bytesRange) - whenever(bytesRange.toHttpRangeHeaderValue()).thenReturn("bytes 200-1000/67589") - whenever(uri.toString()).thenReturn("https://example.com") - state = fetcher.createFetchState(consumer, context) - } - - @Test - @Throws(Exception::class) - fun checkNotNull() { - Assert.assertNotNull(fetcher) - } - - @Test - @Throws(Exception::class) - fun testFetchCaseReturn() { - whenever( - defaultKvStore.getBoolean( - CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, - false, - ), - ).thenReturn(true) - fetcher.fetch(state, callback) - verify(callback).onFailure(any()) - } - - @Test - @Throws(Exception::class) - fun testFetch() { - Whitebox.setInternalState(fetcher, "mCacheControl", mCacheControl) - whenever( - defaultKvStore.getBoolean( - CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, - false, - ), - ).thenReturn(false) - fetcher.fetch(state, callback) - fetcher.onFetchCompletion(state, 0) - verify(bytesRange).toHttpRangeHeaderValue() - } - - @Test - @Throws(Exception::class) - fun testFetchCaseException() { - whenever(uri.toString()).thenReturn("") - whenever( - defaultKvStore.getBoolean( - CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, - false, - ), - ).thenReturn(false) - fetcher.fetch(state, callback) - verify(callback).onFailure(any()) - } - - @Test - @Throws(Exception::class) - fun testGetExtraMap() { - val map = fetcher.getExtraMap(state, 40) - Assertions.assertEquals(map!!["image_size"], 40.toString()) - } - - @Test - @Throws(Exception::class) - fun testOnFetchCancellationRequested() { - Whitebox.setInternalState(fetcher, "mCancellationExecutor", executor) - val method: Method = - CustomOkHttpNetworkFetcher::class.java.getDeclaredMethod( - "onFetchCancellationRequested", - Call::class.java, - ) - method.isAccessible = true - method.invoke(fetcher, call) - verify(executor).execute(any()) - } - - @Test - @Throws(Exception::class) - fun testOnFetchResponseCaseReturn() { - whenever(response.body).thenReturn(body) - whenever(response.isSuccessful).thenReturn(false) - whenever(call.isCanceled()).thenReturn(true) - val method: Method = - CustomOkHttpNetworkFetcher::class.java.getDeclaredMethod( - "onFetchResponse", - OkHttpNetworkFetchState::class.java, - Call::class.java, - Response::class.java, - NetworkFetcher.Callback::class.java, - ) - method.isAccessible = true - method.invoke(fetcher, state, call, response, callback) - verify(callback).onCancellation() - } - - @Test - @Throws(Exception::class) - fun testOnFetchResponse() { - whenever(response.body).thenReturn(body) - whenever(response.isSuccessful).thenReturn(true) - - whenever(call.isCanceled()).thenReturn(true) - whenever(body.contentLength()).thenReturn(-1) - - // Build Response object with Content-Range header - val responseBuilder = - Response - .Builder() - .request(Request.Builder().url("http://example.com").build()) - .protocol(Protocol.HTTP_1_1) - .code(200) - .message("OK") - .header("Content-Range", "bytes 200-1000/67589") - .body(body) - whenever(call.execute()).thenReturn(responseBuilder.build()) - - val method: Method = - CustomOkHttpNetworkFetcher::class.java.getDeclaredMethod( - "onFetchResponse", - OkHttpNetworkFetchState::class.java, - Call::class.java, - Response::class.java, - NetworkFetcher.Callback::class.java, - ) - method.isAccessible = true - method.invoke(fetcher, state, call, responseBuilder.build(), callback) - verify(callback).onResponse(null, 0) - } - - @Test - @Throws(Exception::class) - fun testOnFetchResponseCaseException() { - whenever(response.body).thenReturn(body) - whenever(response.isSuccessful).thenReturn(true) - - whenever(call.isCanceled()).thenReturn(false) - whenever(body.contentLength()).thenReturn(-1) - - // Build Response object with Content-Range header - val responseBuilder = - Response - .Builder() - .request(Request.Builder().url("http://example.com").build()) - .protocol(Protocol.HTTP_1_1) - .code(200) - .message("OK") - .header("Content-Range", "Test") - .body(body) - whenever(call.execute()).thenReturn(responseBuilder.build()) - - val method: Method = - CustomOkHttpNetworkFetcher::class.java.getDeclaredMethod( - "onFetchResponse", - OkHttpNetworkFetchState::class.java, - Call::class.java, - Response::class.java, - NetworkFetcher.Callback::class.java, - ) - method.isAccessible = true - method.invoke(fetcher, state, call, responseBuilder.build(), callback) - verify(callback).onFailure(any()) - } -} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt index 6159a3ccf0f..181ec1de79c 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt @@ -21,10 +21,7 @@ import android.widget.TextView import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction import androidx.test.core.app.ApplicationProvider -import com.facebook.drawee.backends.pipeline.Fresco -import com.facebook.drawee.generic.GenericDraweeHierarchy -import com.facebook.drawee.view.SimpleDraweeView -import com.facebook.soloader.SoLoader +import android.widget.ImageView import com.nhaarman.mockitokotlin2.doReturn import com.nhaarman.mockitokotlin2.whenever import fr.free.nrw.commons.locationpicker.LocationPickerActivity @@ -114,7 +111,7 @@ class MediaDetailFragmentUnitTests { private lateinit var media: Media @Mock - private lateinit var simpleDraweeView: SimpleDraweeView + private lateinit var imageView: ImageView @Mock private lateinit var textView: TextView @@ -125,9 +122,6 @@ class MediaDetailFragmentUnitTests { @Mock private lateinit var linearLayout: LinearLayout - @Mock - private lateinit var genericDraweeHierarchy: GenericDraweeHierarchy - @Mock private lateinit var button: Button @@ -169,10 +163,6 @@ class MediaDetailFragmentUnitTests { context = ApplicationProvider.getApplicationContext() OkHttpConnectionFactory.CLIENT = createTestClient() - SoLoader.setInTestMode() - - Fresco.initialize(ApplicationProvider.getApplicationContext()) - activity = Robolectric.buildActivity(SearchActivity::class.java).create().get() fragment = MediaDetailFragment() @@ -198,14 +188,13 @@ class MediaDetailFragmentUnitTests { Whitebox.setInternalState(fragment, "deleteHelper", deleteHelper) Whitebox.setInternalState(fragment, "_binding", _binding) Whitebox.setInternalState(fragment, "detailProvider", detailProvider) - Whitebox.setInternalState(_binding, "mediaDetailImageView", simpleDraweeView) + Whitebox.setInternalState(_binding, "mediaDetailImageView", imageView) Whitebox.setInternalState(_binding, "mediaDetailTitle", textView) Whitebox.setInternalState(_binding, "mediaDetailDepictionContainer", linearLayout) Whitebox.setInternalState(_binding, "dummyCaptionDescriptionContainer", linearLayout) Whitebox.setInternalState(_binding, "depictionsEditButton", button) Whitebox.setInternalState(fragment, "locationManager", locationManager) - `when`(simpleDraweeView.hierarchy).thenReturn(genericDraweeHierarchy) val map = HashMap() map[Locale.getDefault().language] = "" `when`(media.descriptions).thenReturn(map) @@ -860,7 +849,7 @@ class MediaDetailFragmentUnitTests { spyFragment.onImageBackgroundChanged(color) - verify(simpleDraweeView, times(1)).setBackgroundColor(color) + verify(imageView, times(1)).setBackgroundColor(color) verify(mockSharedPreferencesEditor, times(1)).putInt(anyString(), anyInt()) } @@ -872,7 +861,7 @@ class MediaDetailFragmentUnitTests { doReturn(color).`when`(mockSharedPreferences).getInt(anyString(), anyInt()) spyFragment.onImageBackgroundChanged(color) - verify(simpleDraweeView, never()).setBackgroundColor(anyInt()) + verify(imageView, never()).setBackgroundColor(anyInt()) verify(mockSharedPreferencesEditor, never()).putInt(anyString(), anyInt()) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailPagerFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailPagerFragmentUnitTests.kt index a6c925543f1..b668d00a1f0 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailPagerFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailPagerFragmentUnitTests.kt @@ -10,8 +10,7 @@ import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction import androidx.test.core.app.ApplicationProvider import androidx.work.testing.WorkManagerTestInitHelper -import com.facebook.drawee.backends.pipeline.Fresco -import com.facebook.soloader.SoLoader + import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.doReturn import com.nhaarman.mockitokotlin2.never @@ -86,10 +85,6 @@ class MediaDetailPagerFragmentUnitTests { OkHttpConnectionFactory.CLIENT = createTestClient() - SoLoader.setInTestMode() - - Fresco.initialize(context) - val activity = Robolectric.buildActivity(SearchActivity::class.java).create().get() binding = FragmentMediaDetailPagerBinding.inflate(activity.layoutInflater) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/media/ZoomableActivityUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/media/ZoomableActivityUnitTests.kt index 848e0881aa5..5277d269642 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/media/ZoomableActivityUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/media/ZoomableActivityUnitTests.kt @@ -4,8 +4,7 @@ import android.content.Context import android.content.Intent import android.net.Uri import androidx.test.core.app.ApplicationProvider -import com.facebook.drawee.backends.pipeline.Fresco -import com.facebook.soloader.SoLoader + import fr.free.nrw.commons.OkHttpConnectionFactory import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.createTestClient @@ -45,8 +44,6 @@ class ZoomableActivityUnitTests { MockitoAnnotations.openMocks(this) OkHttpConnectionFactory.CLIENT = createTestClient() context = ApplicationProvider.getApplicationContext() - SoLoader.setInTestMode() - Fresco.initialize(context) val intent = Intent().setData(uri) activity = Robolectric.buildActivity(ZoomableActivity::class.java, intent).create().get() diff --git a/app/src/test/kotlin/fr/free/nrw/commons/media/zoomControllers/MultiPointerGestureDetectorUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/media/zoomControllers/MultiPointerGestureDetectorUnitTest.kt deleted file mode 100644 index 5389c8551df..00000000000 --- a/app/src/test/kotlin/fr/free/nrw/commons/media/zoomControllers/MultiPointerGestureDetectorUnitTest.kt +++ /dev/null @@ -1,179 +0,0 @@ -package fr.free.nrw.commons.media.zoomControllers - -import android.view.MotionEvent -import com.nhaarman.mockitokotlin2.times -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever -import fr.free.nrw.commons.media.zoomControllers.gestures.MultiPointerGestureDetector -import org.junit.Assert -import org.junit.Before -import org.junit.Test -import org.mockito.Mock -import org.mockito.MockitoAnnotations -import org.powermock.reflect.Whitebox -import java.lang.reflect.Method - -class MultiPointerGestureDetectorUnitTest { - private lateinit var detector: MultiPointerGestureDetector - - @Mock - private lateinit var listener: MultiPointerGestureDetector.Listener - - @Mock - private lateinit var event: MotionEvent - - @Before - fun setUp() { - MockitoAnnotations.openMocks(this) - detector = MultiPointerGestureDetector() - detector = MultiPointerGestureDetector.newInstance() - detector.setListener(listener) - - Whitebox.setInternalState(detector, "mGestureInProgress", false) - } - - @Test - @Throws(Exception::class) - fun checkDetectorNotNull() { - Assert.assertNotNull(detector) - } - - @Test - @Throws(Exception::class) - fun testSetAvatarCaseNull() { - val method: Method = - MultiPointerGestureDetector::class.java.getDeclaredMethod( - "shouldStartGesture", - ) - method.isAccessible = true - Assert.assertEquals(method.invoke(detector), true) - } - - @Test - @Throws(Exception::class) - fun testStartGesture() { - val method: Method = - MultiPointerGestureDetector::class.java.getDeclaredMethod( - "startGesture", - ) - method.isAccessible = true - method.invoke(detector) - verify(listener).onGestureBegin(detector) - } - - @Test - @Throws(Exception::class) - fun testStopGesture() { - Whitebox.setInternalState(detector, "mGestureInProgress", true) - val method: Method = - MultiPointerGestureDetector::class.java.getDeclaredMethod( - "stopGesture", - ) - method.isAccessible = true - method.invoke(detector) - verify(listener).onGestureEnd(detector) - } - - @Test - @Throws(Exception::class) - fun testUpdatePointersOnTap() { - whenever(event.pointerCount).thenReturn(3) - whenever(event.actionMasked).thenReturn(MotionEvent.ACTION_UP) - val method: Method = - MultiPointerGestureDetector::class.java.getDeclaredMethod( - "updatePointersOnTap", - MotionEvent::class.java, - ) - method.isAccessible = true - method.invoke(detector, event) - verify(event, times(2)).actionIndex - } - - @Test - @Throws(Exception::class) - fun testRestartGestureCaseReturn() { - detector.restartGesture() - } - - @Test - @Throws(Exception::class) - fun testRestartGesture() { - Whitebox.setInternalState(detector, "mGestureInProgress", true) - detector.restartGesture() - verify(listener).onGestureBegin(detector) - verify(listener).onGestureEnd(detector) - } - - @Test - @Throws(Exception::class) - fun testIsGestureInProgress() { - Assert.assertEquals(detector.isGestureInProgress(), false) - } - - @Test - @Throws(Exception::class) - fun testGetNewPointerCount() { - Assert.assertEquals(detector.getNewPointerCount(), 0) - } - - @Test - @Throws(Exception::class) - fun testGetPointerCount() { - Assert.assertEquals(detector.getPointerCount(), 0) - } - - @Test - @Throws(Exception::class) - fun testGetStartX() { - Assert.assertEquals(detector.getStartX()[0], 0.0f) - } - - @Test - @Throws(Exception::class) - fun testGetStartY() { - Assert.assertEquals(detector.getStartY()[0], 0.0f) - } - - @Test - @Throws(Exception::class) - fun testGetCurrentX() { - Assert.assertEquals(detector.getCurrentX()[0], 0.0f) - } - - @Test - @Throws(Exception::class) - fun testGetCurrentY() { - Assert.assertEquals(detector.getCurrentY()[0], 0.0f) - } - - @Test - @Throws(Exception::class) - fun testOnTouchEvent() { - Assert.assertEquals(detector.onTouchEvent(event), true) - } - - @Test - @Throws(Exception::class) - fun `testOnTouchEventCase ACTION_MOVE`() { - Whitebox.setInternalState(detector, "mPointerCount", 1) - whenever(event.actionMasked).thenReturn(MotionEvent.ACTION_MOVE) - Assert.assertEquals(detector.onTouchEvent(event), true) - } - - @Test - @Throws(Exception::class) - fun `testOnTouchEventCase ACTION_UP`() { - whenever(event.pointerCount).thenReturn(3) - Whitebox.setInternalState(detector, "mPointerCount", 1) - whenever(event.actionMasked).thenReturn(MotionEvent.ACTION_UP) - whenever(event.pointerCount).thenReturn(-2) - Assert.assertEquals(detector.onTouchEvent(event), true) - } - - @Test - @Throws(Exception::class) - fun `testOnTouchEventCase ACTION_CANCEL`() { - whenever(event.actionMasked).thenReturn(MotionEvent.ACTION_CANCEL) - Assert.assertEquals(detector.onTouchEvent(event), true) - } -} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/media/zoomControllers/TransformGestureDetectorUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/media/zoomControllers/TransformGestureDetectorUnitTest.kt deleted file mode 100644 index 3c6af2f56d8..00000000000 --- a/app/src/test/kotlin/fr/free/nrw/commons/media/zoomControllers/TransformGestureDetectorUnitTest.kt +++ /dev/null @@ -1,187 +0,0 @@ -package fr.free.nrw.commons.media.zoomControllers - -import android.view.MotionEvent -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever -import fr.free.nrw.commons.media.zoomControllers.gestures.MultiPointerGestureDetector -import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector -import org.junit.Assert -import org.junit.Before -import org.junit.Test -import org.mockito.Mock -import org.mockito.MockitoAnnotations -import org.powermock.reflect.Whitebox -import java.lang.reflect.Method - -class TransformGestureDetectorUnitTest { - private lateinit var detector: TransformGestureDetector - - @Mock - private lateinit var listener: TransformGestureDetector.Listener - - @Mock - private lateinit var mDetector: MultiPointerGestureDetector - - @Mock - private lateinit var event: MotionEvent - - @Before - fun setUp() { - MockitoAnnotations.openMocks(this) - detector = TransformGestureDetector(MultiPointerGestureDetector()) - detector = TransformGestureDetector.newInstance() - detector.setListener(listener) - } - - @Test - @Throws(Exception::class) - fun checkDetectorNotNull() { - Assert.assertNotNull(detector) - } - - @Test - @Throws(Exception::class) - fun testReset() { - Whitebox.setInternalState(detector, "mDetector", mDetector) - detector.reset() - verify(mDetector).reset() - } - - @Test - @Throws(Exception::class) - fun testOnTouchEvent() { - whenever(mDetector.onTouchEvent(event)).thenReturn(true) - Assert.assertEquals(detector.onTouchEvent(event), true) - } - - @Test - @Throws(Exception::class) - fun testOnGestureBegin() { - detector.onGestureBegin(mDetector) - verify(listener).onGestureBegin(detector) - } - - @Test - @Throws(Exception::class) - fun testOnGestureUpdate() { - detector.onGestureUpdate(mDetector) - verify(listener).onGestureUpdate(detector) - } - - @Test - @Throws(Exception::class) - fun testOnGestureEnd() { - detector.onGestureEnd(mDetector) - verify(listener).onGestureEnd(detector) - } - - @Test - @Throws(Exception::class) - fun testRestartGesture() { - detector.restartGesture() - } - - @Test - @Throws(Exception::class) - fun testIsGestureInProgress() { - Assert.assertEquals(detector.isGestureInProgress(), false) - } - - @Test - @Throws(Exception::class) - fun testGetNewPointerCount() { - Assert.assertEquals(detector.getNewPointerCount(), 0) - } - - @Test - @Throws(Exception::class) - fun testGetPointerCount() { - Assert.assertEquals(detector.getPointerCount(), 0) - } - - @Test - @Throws(Exception::class) - fun testGetPivotX() { - Assert.assertEquals(detector.getPivotX(), 0.0f) - } - - @Test - @Throws(Exception::class) - fun testGetPivotY() { - Assert.assertEquals(detector.getPivotY(), 0.0f) - } - - @Test - @Throws(Exception::class) - fun testGetTranslationX() { - Assert.assertEquals(detector.getTranslationX(), 0.0f) - } - - @Test - @Throws(Exception::class) - fun testGetTranslationY() { - Assert.assertEquals(detector.getTranslationY(), 0.0f) - } - - @Test - @Throws(Exception::class) - fun testGetScaleCaseLessThan2() { - Whitebox.setInternalState(detector, "mDetector", mDetector) - whenever(mDetector.getPointerCount()).thenReturn(1) - Assert.assertEquals(detector.getScale(), 1f) - } - - @Test - @Throws(Exception::class) - fun testGetScaleCaseGreaterThan2() { - val array = FloatArray(2) - array[0] = 0.0f - array[1] = 1.0f - Whitebox.setInternalState(detector, "mDetector", mDetector) - whenever(mDetector.getPointerCount()).thenReturn(2) - whenever(mDetector.getStartX()).thenReturn(array) - whenever(mDetector.getStartY()).thenReturn(array) - whenever(mDetector.getCurrentX()).thenReturn(array) - whenever(mDetector.getCurrentY()).thenReturn(array) - Assert.assertEquals(detector.getScale(), 1f) - } - - @Test - @Throws(Exception::class) - fun testGetRotationCaseLessThan2() { - Whitebox.setInternalState(detector, "mDetector", mDetector) - whenever(mDetector.getPointerCount()).thenReturn(1) - Assert.assertEquals(detector.getRotation(), 0f) - } - - @Test - @Throws(Exception::class) - fun testGetRotationCaseGreaterThan2() { - val array = FloatArray(2) - array[0] = 0.0f - array[1] = 1.0f - Whitebox.setInternalState(detector, "mDetector", mDetector) - whenever(mDetector.getPointerCount()).thenReturn(2) - whenever(mDetector.getStartX()).thenReturn(array) - whenever(mDetector.getStartY()).thenReturn(array) - whenever(mDetector.getCurrentX()).thenReturn(array) - whenever(mDetector.getCurrentY()).thenReturn(array) - Assert.assertEquals(detector.getRotation(), 0f) - } - - @Test - @Throws(Exception::class) - fun testCalcAverage() { - val array = FloatArray(2) - array[0] = 0.0f - array[1] = 1.0f - val method: Method = - TransformGestureDetector::class.java.getDeclaredMethod( - "calcAverage", - FloatArray::class.java, - Int::class.java, - ) - method.isAccessible = true - Assert.assertEquals(method.invoke(detector, array, 2), 0.5f) - } -} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/quiz/QuizActivityUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/quiz/QuizActivityUnitTest.kt index a0508ea5ea5..19adbbb2470 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/quiz/QuizActivityUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/quiz/QuizActivityUnitTest.kt @@ -4,9 +4,7 @@ import android.content.Context import android.view.LayoutInflater import android.view.View import android.widget.Button -import androidx.test.core.app.ApplicationProvider -import com.facebook.drawee.backends.pipeline.Fresco -import com.facebook.soloader.SoLoader + import fr.free.nrw.commons.R import fr.free.nrw.commons.TestCommonsApplication import org.junit.Assert @@ -40,8 +38,6 @@ class QuizActivityUnitTest { @Before fun setUp() { MockitoAnnotations.openMocks(this) - SoLoader.setInTestMode() - Fresco.initialize(ApplicationProvider.getApplicationContext()) activity = Robolectric.buildActivity(QuizActivity::class.java).create().get() context = mock(Context::class.java) view = diff --git a/app/src/test/kotlin/fr/free/nrw/commons/quiz/QuizCheckerUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/quiz/QuizCheckerUnitTest.kt index 0b7498592cc..02112c21052 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/quiz/QuizCheckerUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/quiz/QuizCheckerUnitTest.kt @@ -1,9 +1,7 @@ package fr.free.nrw.commons.quiz import android.app.Activity -import androidx.test.core.app.ApplicationProvider -import com.facebook.drawee.backends.pipeline.Fresco -import com.facebook.soloader.SoLoader + import com.nhaarman.mockitokotlin2.any import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.auth.SessionManager @@ -45,8 +43,6 @@ class QuizCheckerUnitTest { @Before fun setUp() { MockitoAnnotations.openMocks(this) - SoLoader.setInTestMode() - Fresco.initialize(ApplicationProvider.getApplicationContext()) activity = Robolectric.buildActivity(QuizActivity::class.java).create().get() quizChecker = QuizChecker(sessionManager, okHttpJsonApiClient, jsonKvStore) Mockito.`when`(sessionManager.userName).thenReturn("") diff --git a/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewActivityTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewActivityTest.kt index f2faf769acc..4371922fee5 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewActivityTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewActivityTest.kt @@ -5,8 +5,7 @@ import android.os.Looper.getMainLooper import android.view.Menu import android.view.MenuItem import androidx.test.core.app.ApplicationProvider -import com.facebook.drawee.backends.pipeline.Fresco -import com.facebook.soloader.SoLoader + import com.nhaarman.mockitokotlin2.doNothing import fr.free.nrw.commons.Media import fr.free.nrw.commons.OkHttpConnectionFactory @@ -70,10 +69,6 @@ class ReviewActivityTest { OkHttpConnectionFactory.CLIENT = createTestClient() - SoLoader.setInTestMode() - - Fresco.initialize(context) - activity = Robolectric.buildActivity(ReviewActivity::class.java).create().get() binding = ActivityReviewBinding.inflate(activity.layoutInflater) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewControllerTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewControllerTest.kt index 08237c5d924..2a7e761266b 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewControllerTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewControllerTest.kt @@ -4,8 +4,7 @@ import android.app.Activity import android.content.Context import android.os.Looper import androidx.test.core.app.ApplicationProvider -import com.facebook.drawee.backends.pipeline.Fresco -import com.facebook.soloader.SoLoader + import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import fr.free.nrw.commons.Media @@ -58,8 +57,6 @@ class ReviewControllerTest { MockitoAnnotations.openMocks(this) context = ApplicationProvider.getApplicationContext() OkHttpConnectionFactory.CLIENT = createTestClient() - SoLoader.setInTestMode() - Fresco.initialize(context) activity = Robolectric.buildActivity(ReviewActivity::class.java).create().get() controller = ReviewController(deleteHelper, context) media = media(filename = "test_file", dateUploaded = Date()) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewImageFragmentTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewImageFragmentTest.kt index bf4bd18784f..cb687a31471 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewImageFragmentTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewImageFragmentTest.kt @@ -10,8 +10,7 @@ import android.widget.TextView import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction import androidx.test.core.app.ApplicationProvider -import com.facebook.drawee.backends.pipeline.Fresco -import com.facebook.soloader.SoLoader + import com.nhaarman.mockitokotlin2.doReturn import fr.free.nrw.commons.Media import fr.free.nrw.commons.OkHttpConnectionFactory @@ -65,9 +64,7 @@ class ReviewImageFragmentTest { MockitoAnnotations.openMocks(this) context = ApplicationProvider.getApplicationContext() OkHttpConnectionFactory.CLIENT = createTestClient() - SoLoader.setInTestMode() - Fresco.initialize(context) activity = Robolectric.buildActivity(ReviewActivity::class.java).create().get() fragment = ReviewImageFragment() val bundle = Bundle() diff --git a/app/src/test/kotlin/fr/free/nrw/commons/widget/PicOfDayAppWidgetUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/widget/PicOfDayAppWidgetUnitTests.kt index 623bd2affee..3f934c63fa5 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/widget/PicOfDayAppWidgetUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/widget/PicOfDayAppWidgetUnitTests.kt @@ -4,8 +4,6 @@ import android.appwidget.AppWidgetManager import android.content.Context import android.widget.RemoteViews import androidx.test.core.app.ApplicationProvider -import com.facebook.imagepipeline.core.ImagePipelineFactory -import com.facebook.soloader.SoLoader import fr.free.nrw.commons.Media import fr.free.nrw.commons.OkHttpConnectionFactory import fr.free.nrw.commons.TestCommonsApplication @@ -47,8 +45,6 @@ class PicOfDayAppWidgetUnitTests { fun setUp() { OkHttpConnectionFactory.CLIENT = createTestClient() context = ApplicationProvider.getApplicationContext() - SoLoader.setInTestMode() - ImagePipelineFactory.initialize(context) MockitoAnnotations.openMocks(this) widget = PicOfDayAppWidget() Whitebox.setInternalState(widget, "compositeDisposable", compositeDisposable) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f9ae80547be..f9544f5746d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,9 +27,8 @@ dexterVersion = "5.0.0" espresso = "3.6.1" exifinterface = "1.3.7" fragmentTesting = "1.6.2" -frescoVersion = "3.6.0" +coil = "3.1.0" commonsLang3Version = "3.8.1" -glide = "4.12.0" gson = "2.8.5" junit = "4.13.2" junitJupiter = "5.10.0" @@ -66,7 +65,6 @@ rxjava = "2.2.3" rxbinding = "2.1.1" rxbindingAppcompat = "3.0.0" slf4jApi = "1.7.25" -soloader = "0.10.5" timber = "4.7.1" uiautomator = "2.2.0" workManager = "2.8.1" @@ -125,10 +123,8 @@ dagger-android = { module = "com.google.dagger:dagger-android", version.ref = "d dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" } # Image loading -facebook-fresco = { module = "com.facebook.fresco:fresco", version.ref = "frescoVersion" } -facebook-fresco-middleware = { module = "com.facebook.fresco:middleware", version.ref = "frescoVersion" } -glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" } -glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } +coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } +coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } kotlinx-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2", version.ref = "kotlinxCoroutinesRx2" } photoview = { module = "com.github.chrisbanes:PhotoView", version.ref = "photoviewVersion" } @@ -152,7 +148,6 @@ junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.re kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } livedata-testing-ktx = { module = "com.jraska.livedata:testing-ktx", version.ref = "livedataTesting" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } -soloader = { module = "com.facebook.soloader:soloader", version.ref = "soloader" } # Mocking mockk = { module = "io.mockk:mockk", version.ref = "mockk" }