forked from hardcoresushi/DroidFS
Limit the number of thumbnails loaded concurrently
This commit is contained in:
parent
8776d2ee28
commit
1727170cb6
92
app/src/main/java/sushi/hardcore/droidfs/ThumbnailsLoader.kt
Normal file
92
app/src/main/java/sushi/hardcore/droidfs/ThumbnailsLoader.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user