From 1727170cb6126e13bd3ec322454223ed366071a1 Mon Sep 17 00:00:00 2001 From: Hardcore Sushi Date: Sun, 13 Aug 2023 22:37:44 +0200 Subject: [PATCH] Limit the number of thumbnails loaded concurrently --- .../hardcore/droidfs/ThumbnailsLoader.kt | 92 +++++++++++++++++++ .../adapters/ExplorerElementAdapter.kt | 56 ++++------- 2 files changed, 109 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/sushi/hardcore/droidfs/ThumbnailsLoader.kt diff --git a/app/src/main/java/sushi/hardcore/droidfs/ThumbnailsLoader.kt b/app/src/main/java/sushi/hardcore/droidfs/ThumbnailsLoader.kt new file mode 100644 index 0000000..9a0e111 --- /dev/null +++ b/app/src/main/java/sushi/hardcore/droidfs/ThumbnailsLoader.kt @@ -0,0 +1,92 @@ +package sushi.hardcore.droidfs + +import android.content.Context +import android.graphics.drawable.Drawable +import android.widget.ImageView +import androidx.lifecycle.LifecycleCoroutineScope +import com.bumptech.glide.Glide +import com.bumptech.glide.request.target.DrawableImageViewTarget +import com.bumptech.glide.request.transition.Transition +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.yield +import sushi.hardcore.droidfs.filesystems.EncryptedVolume + +class ThumbnailsLoader( + private val context: Context, + private val encryptedVolume: EncryptedVolume, + private val maxSize: Long, + private val lifecycleScope: LifecycleCoroutineScope +) { + internal class ThumbnailData(val id: Int, val path: String, val imageView: ImageView, val onLoaded: (Drawable) -> Unit) + internal class ThumbnailTask(var senderJob: Job?, var workerJob: Job?, var target: DrawableImageViewTarget?) + + private val concurrentTasks = Runtime.getRuntime().availableProcessors()/4 + private val channel = Channel(concurrentTasks) + private var taskId = 0 + private val tasks = HashMap() + + private suspend fun loadThumbnail(data: ThumbnailData) { + withContext(Dispatchers.IO) { + encryptedVolume.loadWholeFile(data.path, maxSize = maxSize).first?.let { + yield() + withContext(Dispatchers.Main) { + tasks[data.id]?.let { task -> + val channel = Channel(1) + task.target = Glide.with(context).load(it).skipMemoryCache(true).into(object : DrawableImageViewTarget(data.imageView) { + override fun onResourceReady( + resource: Drawable, + transition: Transition? + ) { + super.onResourceReady(resource, transition) + data.onLoaded(resource) + channel.trySend(Unit) + } + }) + channel.receive() + tasks.remove(data.id) + } + } + } + } + } + + fun initialize() { + for (i in 0 until concurrentTasks) { + lifecycleScope.launch { + while (true) { + val data = channel.receive() + val workerJob = launch { + loadThumbnail(data) + } + tasks[data.id]?.workerJob = workerJob + workerJob.join() + } + } + } + } + + fun loadAsync(path: String, target: ImageView, onLoaded: (Drawable) -> Unit): Int { + val id = taskId++ + tasks[id] = ThumbnailTask(null, null, null) + val senderJob = lifecycleScope.launch { + channel.send(ThumbnailData(id, path, target, onLoaded)) + } + tasks[id]!!.senderJob = senderJob + return id + } + + fun cancel(id: Int) { + tasks[id]?.let { task -> + task.senderJob?.cancel() + task.workerJob?.cancel() + task.target?.let { + Glide.with(context).clear(it) + } + } + tasks.remove(id) + } +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/adapters/ExplorerElementAdapter.kt b/app/src/main/java/sushi/hardcore/droidfs/adapters/ExplorerElementAdapter.kt index fd75151..10029f5 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/adapters/ExplorerElementAdapter.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/adapters/ExplorerElementAdapter.kt @@ -2,7 +2,6 @@ package sushi.hardcore.droidfs.adapters import android.annotation.SuppressLint import android.graphics.Bitmap -import android.graphics.drawable.Drawable import android.util.LruCache import android.view.View import android.view.ViewGroup @@ -11,13 +10,12 @@ import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.core.graphics.drawable.toBitmap +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.Glide -import com.bumptech.glide.request.target.DrawableImageViewTarget -import com.bumptech.glide.request.transition.Transition import kotlinx.coroutines.* import sushi.hardcore.droidfs.FileTypes import sushi.hardcore.droidfs.R +import sushi.hardcore.droidfs.ThumbnailsLoader import sushi.hardcore.droidfs.explorers.ExplorerElement import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.Stat @@ -29,7 +27,7 @@ class ExplorerElementAdapter( val activity: AppCompatActivity, val encryptedVolume: EncryptedVolume?, private val listener: Listener, - val thumbnailMaxSize: Long, + thumbnailMaxSize: Long, ) : SelectableAdapter(listener::onSelectionChanged) { val dateFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.getDefault()) var explorerElements = listOf() @@ -40,12 +38,18 @@ class ExplorerElementAdapter( notifyDataSetChanged() } var isUsingListLayout = true + private var thumbnailsLoader: ThumbnailsLoader? = null private var thumbnailsCache: LruCache? = null var loadThumbnails = true init { if (encryptedVolume != null) { - thumbnailsCache = LruCache((Runtime.getRuntime().maxMemory() / 1024 / 8).toInt()) + thumbnailsLoader = ThumbnailsLoader(activity, encryptedVolume, thumbnailMaxSize, activity.lifecycleScope).apply { + initialize() + } + thumbnailsCache = object : LruCache((Runtime.getRuntime().maxMemory() / 4).toInt()) { + override fun sizeOf(key: String, value: Bitmap) = value.byteCount + } } } @@ -115,40 +119,11 @@ class ExplorerElementAdapter( } class FileViewHolder(itemView: View) : RegularElementViewHolder(itemView) { - private var target: DrawableImageViewTarget? = null - private var job: Job? = null - private val scope = CoroutineScope(Dispatchers.IO) - - private fun loadThumbnail(fullPath: String, adapter: ExplorerElementAdapter) { - adapter.encryptedVolume?.let { volume -> - job = scope.launch { - volume.loadWholeFile(fullPath, maxSize = adapter.thumbnailMaxSize).first?.let { - if (isActive) { - withContext(Dispatchers.Main) { - if (isActive && !adapter.activity.isFinishing && !adapter.activity.isDestroyed) { - target = Glide.with(adapter.activity).load(it).skipMemoryCache(true).into(object : DrawableImageViewTarget(icon) { - override fun onResourceReady( - resource: Drawable, - transition: Transition? - ) { - target = null - val bitmap = resource.toBitmap() - adapter.thumbnailsCache!!.put(fullPath, bitmap.copy(bitmap.config, true)) - super.onResourceReady(resource, transition) - } - }) - } - } - } - } - } - } - } + private var task = -1 fun cancelThumbnailLoading(adapter: ExplorerElementAdapter) { - job?.cancel() - target?.let { - Glide.with(adapter.activity).clear(it) + if (task != -1) { + adapter.thumbnailsLoader?.cancel(task) } } @@ -161,7 +136,10 @@ class ExplorerElementAdapter( icon.setImageBitmap(thumbnail) setDefaultIcon = false } else if (adapter.loadThumbnails) { - loadThumbnail(fullPath, adapter) + task = adapter.thumbnailsLoader!!.loadAsync(fullPath, icon) { resource -> + val bitmap = resource.toBitmap() + adapter.thumbnailsCache!!.put(fullPath, bitmap.copy(bitmap.config, true)) + } } } }