Limit the number of thumbnails loaded concurrently

This commit is contained in:
Matéo Duparc 2023-08-13 22:37:44 +02:00
parent 8776d2ee28
commit 1727170cb6
Signed by untrusted user: hardcoresushi
GPG Key ID: AFE384344A45E13A
2 changed files with 109 additions and 39 deletions

View File

@ -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<ThumbnailData>(concurrentTasks)
private var taskId = 0
private val tasks = HashMap<Int, ThumbnailTask>()
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<Unit>(1)
task.target = Glide.with(context).load(it).skipMemoryCache(true).into(object : DrawableImageViewTarget(data.imageView) {
override fun onResourceReady(
resource: Drawable,
transition: Transition<in Drawable>?
) {
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)
}
}

View File

@ -2,7 +2,6 @@ package sushi.hardcore.droidfs.adapters
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.util.LruCache import android.util.LruCache
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -11,13 +10,12 @@ import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView 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 kotlinx.coroutines.*
import sushi.hardcore.droidfs.FileTypes import sushi.hardcore.droidfs.FileTypes
import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.ThumbnailsLoader
import sushi.hardcore.droidfs.explorers.ExplorerElement import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.Stat import sushi.hardcore.droidfs.filesystems.Stat
@ -29,7 +27,7 @@ class ExplorerElementAdapter(
val activity: AppCompatActivity, val activity: AppCompatActivity,
val encryptedVolume: EncryptedVolume?, val encryptedVolume: EncryptedVolume?,
private val listener: Listener, private val listener: Listener,
val thumbnailMaxSize: Long, thumbnailMaxSize: Long,
) : SelectableAdapter<ExplorerElement>(listener::onSelectionChanged) { ) : SelectableAdapter<ExplorerElement>(listener::onSelectionChanged) {
val dateFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.getDefault()) val dateFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.getDefault())
var explorerElements = listOf<ExplorerElement>() var explorerElements = listOf<ExplorerElement>()
@ -40,12 +38,18 @@ class ExplorerElementAdapter(
notifyDataSetChanged() notifyDataSetChanged()
} }
var isUsingListLayout = true var isUsingListLayout = true
private var thumbnailsLoader: ThumbnailsLoader? = null
private var thumbnailsCache: LruCache<String, Bitmap>? = null private var thumbnailsCache: LruCache<String, Bitmap>? = null
var loadThumbnails = true var loadThumbnails = true
init { init {
if (encryptedVolume != null) { if (encryptedVolume != null) {
thumbnailsCache = LruCache((Runtime.getRuntime().maxMemory() / 1024 / 8).toInt()) thumbnailsLoader = ThumbnailsLoader(activity, encryptedVolume, thumbnailMaxSize, activity.lifecycleScope).apply {
initialize()
}
thumbnailsCache = object : LruCache<String, Bitmap>((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) { class FileViewHolder(itemView: View) : RegularElementViewHolder(itemView) {
private var target: DrawableImageViewTarget? = null private var task = -1
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<in Drawable>?
) {
target = null
val bitmap = resource.toBitmap()
adapter.thumbnailsCache!!.put(fullPath, bitmap.copy(bitmap.config, true))
super.onResourceReady(resource, transition)
}
})
}
}
}
}
}
}
}
fun cancelThumbnailLoading(adapter: ExplorerElementAdapter) { fun cancelThumbnailLoading(adapter: ExplorerElementAdapter) {
job?.cancel() if (task != -1) {
target?.let { adapter.thumbnailsLoader?.cancel(task)
Glide.with(adapter.activity).clear(it)
} }
} }
@ -161,7 +136,10 @@ class ExplorerElementAdapter(
icon.setImageBitmap(thumbnail) icon.setImageBitmap(thumbnail)
setDefaultIcon = false setDefaultIcon = false
} else if (adapter.loadThumbnails) { } 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))
}
} }
} }
} }