From d837ae6a10f55e330d351c6faff3433e8b37bf51 Mon Sep 17 00:00:00 2001 From: Dinar Khakimov <85668474+mdrlzy@users.noreply.github.com> Date: Wed, 15 Nov 2023 22:15:58 +0600 Subject: [PATCH] #73: Replace Glide with Coil (#115) And bump compile sdk version --- lib/build.gradle | 15 +- .../meta/generator/ImageMetadataGenerator.kt | 5 +- .../arklib/data/preview/Preview.kt | 52 ++++--- .../data/preview/RootPreviewProcessor.kt | 6 +- .../generator/ImagePreviewGenerator.kt | 11 +- .../generator/VideoPreviewGenerator.kt | 25 ++- .../arkbuilders/arklib/utils/ImageUtils.kt | 142 +++++++++--------- 7 files changed, 141 insertions(+), 115 deletions(-) diff --git a/lib/build.gradle b/lib/build.gradle index 32f6770c..14e2fc1d 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -20,7 +20,7 @@ sonarqube { } } android { - compileSdkVersion 32 + compileSdkVersion 34 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -28,7 +28,7 @@ android { defaultConfig { minSdkVersion 26 - targetSdkVersion 31 + targetSdkVersion 34 } buildTypes { @@ -73,10 +73,13 @@ dependencies { implementation "com.google.dagger:dagger:2.41" implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.github.wseemann:FFmpegMediaMetadataRetriever:1.0.14' - implementation 'com.github.MikeOrtiz:TouchImageView:3.1.1' - implementation "com.github.bumptech.glide:glide:4.11.0" - kapt "com.github.bumptech.glide:compiler:4.11.0" - + + def coilVersion = "2.4.0" + implementation "io.coil-kt:coil:$coilVersion" + implementation "io.coil-kt:coil-gif:$coilVersion" + implementation "io.coil-kt:coil-svg:$coilVersion" + implementation "io.coil-kt:coil-video:$coilVersion" + testImplementation "junit:junit:4.13.2" testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3' testImplementation "io.mockk:mockk:1.13.7" diff --git a/lib/src/main/java/dev/arkbuilders/arklib/data/meta/generator/ImageMetadataGenerator.kt b/lib/src/main/java/dev/arkbuilders/arklib/data/meta/generator/ImageMetadataGenerator.kt index 4ed30353..3171222d 100644 --- a/lib/src/main/java/dev/arkbuilders/arklib/data/meta/generator/ImageMetadataGenerator.kt +++ b/lib/src/main/java/dev/arkbuilders/arklib/data/meta/generator/ImageMetadataGenerator.kt @@ -11,7 +11,7 @@ object ImageMetadataGenerator: MetadataGenerator { get() = setOf("bmp", "gif", "ico", "jpg", "jpeg", "png", "tif", "tiff", "webp", "heic", "heif", - "avif") + "avif", "svg") override val acceptedMimeTypes: Set get() = setOf( @@ -24,7 +24,8 @@ object ImageMetadataGenerator: MetadataGenerator { "image/tiff", "image/webp", "image/heic", - "image/avif" + "image/avif", + "image/svg+xml" ) override fun generate(path: Path, resource: Resource): Result = diff --git a/lib/src/main/java/dev/arkbuilders/arklib/data/preview/Preview.kt b/lib/src/main/java/dev/arkbuilders/arklib/data/preview/Preview.kt index 4a09b28c..d08970f4 100644 --- a/lib/src/main/java/dev/arkbuilders/arklib/data/preview/Preview.kt +++ b/lib/src/main/java/dev/arkbuilders/arklib/data/preview/Preview.kt @@ -1,11 +1,12 @@ package dev.arkbuilders.arklib.data.preview import android.graphics.Bitmap -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestBuilder -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy -import com.bumptech.glide.request.RequestOptions +import android.util.Log +import androidx.core.graphics.drawable.toBitmap +import coil.ImageLoader +import coil.request.ImageRequest +import coil.size.Precision +import coil.size.Scale import kotlinx.coroutines.Job import dev.arkbuilders.arklib.* import dev.arkbuilders.arklib.app @@ -29,27 +30,35 @@ data class Preview( // we don't have fullscreen preview for // some kinds of resources, e.g. Image or PlainText - val onlyThumbnail: Boolean) { + val onlyThumbnail: Boolean +) { internal companion object { const val THUMBNAIL_SIZE = 128 const val COMPRESSION_QUALITY = 100 - fun downscale(bitmap: Bitmap): Bitmap = - downscale(Glide.with(app).asBitmap().load(bitmap)) - - fun downscale(builder: RequestBuilder): Bitmap = - builder - .apply(RequestOptions() - .downsample(DownsampleStrategy.CENTER_INSIDE) - .override(THUMBNAIL_SIZE) - .fitCenter() - .diskCacheStrategy(DiskCacheStrategy.NONE) - .skipMemoryCache(true) + /** + * See [ImageRequest.Builder.data] for supported data types + * Note: pass [java.io.File] instead of [java.nio.file.Path] + */ + suspend fun downscale(resource: Path, data: Any): Result = runCatching { + val request = ImageRequest.Builder(app) + .size(THUMBNAIL_SIZE) + .precision(Precision.EXACT) + .scale(Scale.FIT) + .data(data) + .listener( + onError = { _, result -> + Log.d( + LOG_PREFIX, + "Failed to downscale preview for $resource because ${result.throwable}" + ) + }, ) - .addListener(ImageUtils.glideExceptionListener()) - .submit() - .get() + .build() + + ImageUtils.arkImageLoader.execute(request).drawable!!.toBitmap() + } } } @@ -67,7 +76,8 @@ class PreviewLocator( private val previewsDir = root.arkFolder().arkPreviews() private val thumbnailsDir = root.arkFolder().arkThumbnails() - fun fullscreen(): Path = processor.images[id] ?: previewsDir.resolve(id.toString()) + fun fullscreen(): Path = + processor.images[id] ?: previewsDir.resolve(id.toString()) fun thumbnail(): Path = thumbnailsDir.resolve(id.toString()) diff --git a/lib/src/main/java/dev/arkbuilders/arklib/data/preview/RootPreviewProcessor.kt b/lib/src/main/java/dev/arkbuilders/arklib/data/preview/RootPreviewProcessor.kt index 4fd0a1bd..0a6f5ac3 100644 --- a/lib/src/main/java/dev/arkbuilders/arklib/data/preview/RootPreviewProcessor.kt +++ b/lib/src/main/java/dev/arkbuilders/arklib/data/preview/RootPreviewProcessor.kt @@ -105,8 +105,10 @@ class RootPreviewProcessor private constructor( previews.saveBitmap(id, it.bitmap) - val thumbnail = Preview.downscale(it.bitmap) - thumbnails.saveBitmap(id, thumbnail) + val thumbnailResult = Preview.downscale(path, it.bitmap) + thumbnailResult.onSuccess { thumbnail -> + thumbnails.saveBitmap(id, thumbnail) + } } .onFailure { Log.w(LOG_PREFIX, "Failed to generate preview for $path") diff --git a/lib/src/main/java/dev/arkbuilders/arklib/data/preview/generator/ImagePreviewGenerator.kt b/lib/src/main/java/dev/arkbuilders/arklib/data/preview/generator/ImagePreviewGenerator.kt index 8d4ffeb6..4927970a 100644 --- a/lib/src/main/java/dev/arkbuilders/arklib/data/preview/generator/ImagePreviewGenerator.kt +++ b/lib/src/main/java/dev/arkbuilders/arklib/data/preview/generator/ImagePreviewGenerator.kt @@ -1,7 +1,5 @@ package dev.arkbuilders.arklib.data.preview.generator -import com.bumptech.glide.Glide -import dev.arkbuilders.arklib.app import dev.arkbuilders.arklib.data.meta.Kind import dev.arkbuilders.arklib.data.meta.Metadata import dev.arkbuilders.arklib.data.preview.Preview @@ -15,10 +13,13 @@ object ImagePreviewGenerator: PreviewGenerator { } override suspend fun generate(path: Path, meta: Metadata): Result { - val bitmap = Preview.downscale( - Glide.with(app).asBitmap().load(path.toFile()) + val thumbnailResult = Preview.downscale( + path, + path.toFile() ) - return Result.success(Preview(bitmap, onlyThumbnail = true)) + return thumbnailResult.map { thumbnail -> + Preview(thumbnail, onlyThumbnail = true) + } } } diff --git a/lib/src/main/java/dev/arkbuilders/arklib/data/preview/generator/VideoPreviewGenerator.kt b/lib/src/main/java/dev/arkbuilders/arklib/data/preview/generator/VideoPreviewGenerator.kt index 6e92b719..50c77171 100644 --- a/lib/src/main/java/dev/arkbuilders/arklib/data/preview/generator/VideoPreviewGenerator.kt +++ b/lib/src/main/java/dev/arkbuilders/arklib/data/preview/generator/VideoPreviewGenerator.kt @@ -4,13 +4,16 @@ import android.graphics.Bitmap import android.media.MediaMetadataRetriever import android.net.Uri import android.util.Log -import com.bumptech.glide.Glide -import com.bumptech.glide.load.engine.DiskCacheStrategy +import androidx.core.graphics.drawable.toBitmap +import coil.request.ImageRequest +import coil.size.Precision +import coil.size.Scale import dev.arkbuilders.arklib.app import dev.arkbuilders.arklib.data.meta.Kind import dev.arkbuilders.arklib.data.meta.Metadata import dev.arkbuilders.arklib.data.preview.Preview import dev.arkbuilders.arklib.data.preview.PreviewGenerator +import dev.arkbuilders.arklib.utils.ImageUtils import java.nio.file.Path import kotlin.io.path.name import wseemann.media.FFmpegMediaMetadataRetriever @@ -26,7 +29,10 @@ object VideoPreviewGenerator : PreviewGenerator { Preview(it, onlyThumbnail = false) } - private fun generateBitmap(path: Path, durationMillis: Long?): Result { + private suspend fun generateBitmap( + path: Path, + durationMillis: Long? + ): Result { val timeMicros = (durationMillis ?: 10000) / 1000 / 2 val retriever = FFmpegMediaMetadataRetriever() @@ -36,7 +42,7 @@ object VideoPreviewGenerator : PreviewGenerator { // Trying 3 ways to get preview image for video. // 1. using FFmpegMediaMetadataRetriever // 2. using MediaMetadataRetriever - // 3. using Glide + // 3. using Coil val mainBitmap = retriever.frameAtTime ?: let { MediaMetadataRetriever().let { mediaMetadataRetriever -> try { @@ -61,10 +67,13 @@ object VideoPreviewGenerator : PreviewGenerator { } } bitmap - } ?: Glide.with(app.baseContext).asBitmap() - .load(path.toFile()) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .submit().get() + } ?: let { + val request = ImageRequest.Builder(app) + .data(path.toFile()) + .build() + + ImageUtils.arkImageLoader.execute(request).drawable!!.toBitmap() + } } if (mainBitmap != null) { diff --git a/lib/src/main/java/dev/arkbuilders/arklib/utils/ImageUtils.kt b/lib/src/main/java/dev/arkbuilders/arklib/utils/ImageUtils.kt index 0da57c84..adf42d6d 100644 --- a/lib/src/main/java/dev/arkbuilders/arklib/utils/ImageUtils.kt +++ b/lib/src/main/java/dev/arkbuilders/arklib/utils/ImageUtils.kt @@ -1,33 +1,28 @@ package dev.arkbuilders.arklib.utils -import android.graphics.drawable.Drawable +import android.os.Build +import android.os.Build.VERSION.SDK_INT import android.util.Log import android.widget.ImageView -import com.bumptech.glide.Glide -import com.bumptech.glide.Priority -import com.bumptech.glide.load.DataSource -import com.bumptech.glide.load.engine.GlideException -import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy -import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions -import com.bumptech.glide.request.RequestListener -import com.bumptech.glide.request.RequestOptions -import com.bumptech.glide.request.target.CustomTarget -import com.bumptech.glide.request.target.Target -import com.bumptech.glide.request.transition.Transition -import com.bumptech.glide.signature.ObjectKey +import coil.ImageLoader +import coil.decode.GifDecoder +import coil.decode.ImageDecoderDecoder +import coil.decode.SvgDecoder +import coil.decode.VideoFrameDecoder +import coil.load +import coil.request.ImageRequest +import coil.size.Scale import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView -import com.ortiz.touchview.TouchImageView import dev.arkbuilders.arklib.app import dev.arkbuilders.arklib.R import dev.arkbuilders.arklib.ResourceId import java.nio.file.Path object ImageUtils { - private const val MAX_GLIDE_SIZE = 1500 + private const val MAX_SIZE = 1500 private const val PREVIEW_SIGNATURE = "preview" private const val THUMBNAIL_SIGNATURE = "thumbnail" - const val APPEARANCE_DURATION = 300L fun iconForExtension(ext: String): Int { val drawableID = app.resources @@ -41,30 +36,21 @@ object ImageUtils { else R.drawable.ic_file } - fun loadGlideZoomImage(id: ResourceId, image: Path, view: TouchImageView) = - Glide.with(view.context) - .load(image.toFile()) - .apply( - RequestOptions() - .priority(Priority.IMMEDIATE) - .signature(ObjectKey("$id$PREVIEW_SIGNATURE")) - .downsample(DownsampleStrategy.CENTER_INSIDE) - .override(MAX_GLIDE_SIZE) - ) - .into(object : CustomTarget() { - override fun onResourceReady( - resource: Drawable, - transition: Transition? - ) { - view.setImageDrawable(resource) - view.animate().apply { - duration = APPEARANCE_DURATION - alpha(1f) - } - } - - override fun onLoadCleared(placeholder: Drawable?) {} - }) + fun loadImage( + id: ResourceId, + image: Path, + view: ImageView, + limitSize: Boolean + ) { + val signature = "$id$PREVIEW_SIGNATURE" + view.load(image.toFile(), arkImageLoader) { + if (limitSize) + size(MAX_SIZE) + diskCacheKey(signature) + memoryCacheKey(signature) + logListener("[Preview]", id, image) + } + } fun loadSubsamplingImage(image: Path, view: SubsamplingScaleImageView) { view.orientation = SubsamplingScaleImageView.ORIENTATION_USE_EXIF @@ -77,42 +63,56 @@ object ImageUtils { placeHolder: Int, view: ImageView ) { - Log.v(LOG_PREFIX, "loading image $image") + val signature = "$id$THUMBNAIL_SIGNATURE" - Glide.with(view.context) - .load(image?.toFile()) - .placeholder(placeHolder) - .signature(ObjectKey("$id$THUMBNAIL_SIGNATURE")) - .transition(DrawableTransitionOptions.withCrossFade()) - .into(view) + view.load(image?.toFile(), arkImageLoader) { + scale(Scale.FILL) + placeholder(placeHolder) + diskCacheKey(signature) + memoryCacheKey(signature) + crossfade(true) + logListener("[Thumbnail]", id, image) + } } - fun glideExceptionListener() = object : RequestListener { - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target?, - isFirstResource: Boolean - ): Boolean { - Log.w( - LOG_PREFIX, - "load failed with message: ${ - e?.message - } for target of type: ${ - target?.javaClass?.canonicalName - }" - ) - return true - } + fun ImageRequest.Builder.logListener( + prefix: String, + id: ResourceId, + image: Path? + ) { + listener( + onStart = { + Log.d(LOG_PREFIX, "$prefix Start loading path[$image] id[$id]") + }, + onCancel = { + Log.d(LOG_PREFIX, "$prefix Cancel loading path[$image] id[$id]") + }, + onError = { _, result -> + Log.w( + LOG_PREFIX, + "$prefix Error[${result.throwable}] when loading path[$image] id[$id]" + ) + }, + onSuccess = { _, result -> + Log.d(LOG_PREFIX, "$prefix Loaded path[$image] id[$id]") + }, + ) + } - override fun onResourceReady( - resource: T, - model: Any?, - target: Target?, - dataSource: DataSource?, - isFirstResource: Boolean - ) = false + val arkImageLoader by lazy { + ImageLoader.Builder(app) + .components { + if (SDK_INT >= Build.VERSION_CODES.P) { + add(ImageDecoderDecoder.Factory()) + } else { + add(GifDecoder.Factory()) + } + add(SvgDecoder.Factory()) + add(VideoFrameDecoder.Factory()) + } + .build() } + } private const val LOG_PREFIX: String = "[images]"