[android] improve driver fetcher (#251)
All checks were successful
eden-build / source (push) Successful in 6m13s
eden-build / linux (push) Successful in 27m5s
eden-build / windows (msvc) (push) Successful in 30m57s
eden-build / android (push) Successful in 32m9s

- Fix the app compat crash
- Fix kimchi sorting
- Improve performance in ui thread

Signed-off-by: Aleksandr Popovich <alekpopo@pm.me>

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/251
Co-authored-by: Aleksandr Popovich <alekpopo@pm.me>
Co-committed-by: Aleksandr Popovich <alekpopo@pm.me>
This commit is contained in:
Aleksandr Popovich 2025-07-04 20:29:23 +00:00 committed by CamilleLaVey
parent aeb2aec13b
commit b60d0aabf0
No known key found for this signature in database
GPG key ID: BA8734FD0EE46976
3 changed files with 118 additions and 65 deletions

View file

@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.features.fetcher package org.yuzu.yuzu_emu.features.fetcher
import android.annotation.SuppressLint import android.annotation.SuppressLint
@ -17,7 +20,9 @@ import androidx.transition.TransitionManager
import androidx.transition.TransitionSet import androidx.transition.TransitionSet
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.DriverViewModel
class DriverGroupAdapter( class DriverGroupAdapter(
@ -25,43 +30,61 @@ class DriverGroupAdapter(
private val driverViewModel: DriverViewModel private val driverViewModel: DriverViewModel
) : RecyclerView.Adapter<DriverGroupAdapter.DriverGroupViewHolder>() { ) : RecyclerView.Adapter<DriverGroupAdapter.DriverGroupViewHolder>() {
private var driverGroups: List<DriverGroup> = emptyList() private var driverGroups: List<DriverGroup> = emptyList()
private val adapterJobs = mutableMapOf<Int, Job>()
inner class DriverGroupViewHolder( inner class DriverGroupViewHolder(
private val binding: ItemDriverGroupBinding private val binding: ItemDriverGroupBinding
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun bind(group: DriverGroup) { fun bind(group: DriverGroup) {
binding.textGroupName.text = group.name
if (binding.recyclerReleases.layoutManager == null) {
binding.recyclerReleases.layoutManager = LinearLayoutManager(activity)
binding.recyclerReleases.addItemDecoration(
SpacingItemDecoration(
(activity.resources.displayMetrics.density * 8).toInt()
)
)
}
val onClick = { val onClick = {
adapterJobs[bindingAdapterPosition]?.cancel()
TransitionManager.beginDelayedTransition( TransitionManager.beginDelayedTransition(
binding.root, binding.root,
TransitionSet().addTransition(Fade()).addTransition(ChangeBounds()) TransitionSet().addTransition(Fade()).addTransition(ChangeBounds())
.setDuration(200) .setDuration(200)
) )
val isVisible = binding.recyclerReleases.isVisible val isVisible = binding.recyclerReleases.isVisible
if (!isVisible && binding.recyclerReleases.adapter == null) {
val job = CoroutineScope(Dispatchers.Main).launch {
// It prevents blocking the ui thread.
var adapter: ReleaseAdapter?
withContext(Dispatchers.IO) {
adapter = ReleaseAdapter(group.releases, activity, driverViewModel)
}
binding.recyclerReleases.adapter = adapter
}
adapterJobs[bindingAdapterPosition] = job
}
binding.recyclerReleases.visibility = if (isVisible) View.GONE else View.VISIBLE binding.recyclerReleases.visibility = if (isVisible) View.GONE else View.VISIBLE
binding.imageDropdownArrow.rotation = if (isVisible) 0f else 180f binding.imageDropdownArrow.rotation = if (isVisible) 0f else 180f
if (!isVisible && binding.recyclerReleases.adapter == null) {
CoroutineScope(Dispatchers.Main).launch {
binding.recyclerReleases.layoutManager =
LinearLayoutManager(binding.root.context)
binding.recyclerReleases.adapter =
ReleaseAdapter(group.releases, activity, driverViewModel)
binding.recyclerReleases.addItemDecoration(
SpacingItemDecoration(
(activity.resources.displayMetrics.density * 8).toInt()
)
)
}
}
} }
binding.textGroupName.text = group.name
binding.textGroupName.setOnClickListener { onClick() } binding.textGroupName.setOnClickListener { onClick() }
binding.imageDropdownArrow.setOnClickListener { onClick() } binding.imageDropdownArrow.setOnClickListener { onClick() }
} }
fun clear() {
adapterJobs[bindingAdapterPosition]?.cancel()
adapterJobs.remove(bindingAdapterPosition)
}
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverGroupViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverGroupViewHolder {

View file

@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments package org.yuzu.yuzu_emu.fragments
import android.app.AlertDialog
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
@ -28,10 +30,12 @@ import org.yuzu.yuzu_emu.databinding.FragmentDriverFetcherBinding
import org.yuzu.yuzu_emu.features.fetcher.DriverGroupAdapter import org.yuzu.yuzu_emu.features.fetcher.DriverGroupAdapter
import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.utils.GpuDriverHelper import org.yuzu.yuzu_emu.utils.GpuDriverHelper
import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import java.io.IOException import java.io.IOException
import java.net.URL import java.net.URL
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import kotlin.getValue import kotlin.getValue
class DriverFetcherFragment : Fragment() { class DriverFetcherFragment : Fragment() {
@ -49,17 +53,22 @@ class DriverFetcherFragment : Fragment() {
private val recommendedDriver: String private val recommendedDriver: String
get() = driverMap.firstOrNull { adrenoModel in it.first }?.second ?: "Unsupported" get() = driverMap.firstOrNull { adrenoModel in it.first }?.second ?: "Unsupported"
enum class SortMode {
Default, PublishTime,
}
private data class DriverRepo( private data class DriverRepo(
val name: String = "", val name: String = "",
val path: String = "", val path: String = "",
val sort: Int = 0, val sort: Int = 0,
val useTagName: Boolean = false val useTagName: Boolean = false,
val sortMode: SortMode = SortMode.Default,
) )
private val repoList: List<DriverRepo> = listOf( private val repoList: List<DriverRepo> = listOf(
DriverRepo("Mr. Purple Turnip", "MrPurple666/purple-turnip", 0), DriverRepo("Mr. Purple Turnip", "MrPurple666/purple-turnip", 0),
DriverRepo("GameHub Adreno 8xx", "crueter/GameHub-8Elite-Drivers", 1), DriverRepo("GameHub Adreno 8xx", "crueter/GameHub-8Elite-Drivers", 1),
DriverRepo("KIMCHI Turnip", "K11MCH1/AdrenoToolsDrivers", 2, true), DriverRepo("KIMCHI Turnip", "K11MCH1/AdrenoToolsDrivers", 2, true, SortMode.PublishTime),
DriverRepo("Weab-Chan Freedreno", "Weab-chan/freedreno_turnip-CI", 3), DriverRepo("Weab-Chan Freedreno", "Weab-chan/freedreno_turnip-CI", 3),
) )
@ -78,7 +87,7 @@ class DriverFetcherFragment : Fragment() {
private lateinit var driverGroupAdapter: DriverGroupAdapter private lateinit var driverGroupAdapter: DriverGroupAdapter
private val driverViewModel: DriverViewModel by activityViewModels() private val driverViewModel: DriverViewModel by activityViewModels()
fun parseAdrenoModel(): Int { private fun parseAdrenoModel(): Int {
if (gpuModel == null) { if (gpuModel == null) {
return 0 return 0
} }
@ -115,9 +124,8 @@ class DriverFetcherFragment : Fragment() {
} }
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
savedInstanceState: Bundle? ): View {
): View? {
_binding = FragmentDriverFetcherBinding.inflate(inflater) _binding = FragmentDriverFetcherBinding.inflate(inflater)
binding.badgeRecommendedDriver.text = recommendedDriver binding.badgeRecommendedDriver.text = recommendedDriver
binding.badgeGpuModel.text = gpuModel binding.badgeGpuModel.text = gpuModel
@ -150,11 +158,12 @@ class DriverFetcherFragment : Fragment() {
val name = driver.name val name = driver.name
val path = driver.path val path = driver.path
val useTagName = driver.useTagName val useTagName = driver.useTagName
val sortMode = driver.sortMode
val sort = driver.sort val sort = driver.sort
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val request = Request.Builder() val request =
.url("https://api.github.com/repos/$path/releases") Request.Builder().url("https://api.github.com/repos/$path/releases").build()
.build()
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
var releases: ArrayList<Release> var releases: ArrayList<Release>
@ -165,28 +174,25 @@ class DriverFetcherFragment : Fragment() {
} }
val body = response.body?.string() ?: return@withContext val body = response.body?.string() ?: return@withContext
releases = Release.fromJsonArray(body, useTagName) releases = Release.fromJsonArray(body, useTagName, sortMode)
} }
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
MaterialAlertDialogBuilder(requireActivity().applicationContext) MaterialAlertDialogBuilder(requireActivity()).setTitle(getString(R.string.error_during_fetch))
.setTitle(getString(R.string.error_during_fetch))
.setMessage("${getString(R.string.failed_to_fetch)} ${name}:\n${e.message}") .setMessage("${getString(R.string.failed_to_fetch)} ${name}:\n${e.message}")
.setPositiveButton(getString(R.string.ok)) { dialog, _ -> dialog.cancel() } .setPositiveButton(getString(R.string.ok)) { dialog, _ -> dialog.cancel() }
.show() .show()
releases = ArrayList<Release>() releases = ArrayList()
} }
} }
val driver = DriverGroup( val group = DriverGroup(
name, name, releases, sort
releases,
sort
) )
synchronized(driverGroups) { synchronized(driverGroups) {
driverGroups.add(driver) driverGroups.add(group)
driverGroups.sortBy { driverGroups.sortBy {
it.sort it.sort
} }
@ -204,39 +210,41 @@ class DriverFetcherFragment : Fragment() {
} }
} }
private fun setInsets() = private fun setInsets() = ViewCompat.setOnApplyWindowInsetsListener(
ViewCompat.setOnApplyWindowInsetsListener( binding.root
binding.root ) { _: View, windowInsets: WindowInsetsCompat ->
) { _: View, windowInsets: WindowInsetsCompat -> val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val leftInsets = barInsets.left + cutoutInsets.left val leftInsets = barInsets.left + cutoutInsets.left
val rightInsets = barInsets.right + cutoutInsets.right val rightInsets = barInsets.right + cutoutInsets.right
binding.toolbarDrivers.updateMargins(left = leftInsets, right = rightInsets) binding.toolbarDrivers.updateMargins(left = leftInsets, right = rightInsets)
binding.listDrivers.updateMargins(left = leftInsets, right = rightInsets) binding.listDrivers.updateMargins(left = leftInsets, right = rightInsets)
binding.listDrivers.updatePadding( binding.listDrivers.updatePadding(
bottom = barInsets.bottom + bottom = barInsets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab) )
)
windowInsets windowInsets
} }
data class Artifact(val url: URL, val name: String) data class Artifact(val url: URL, val name: String)
data class Release( data class Release(
var tagName: String = "", var tagName: String = "",
var titleName: String = "",
var title: String = "", var title: String = "",
var body: String = "", var body: String = "",
var artifacts: List<Artifact> = ArrayList<Artifact>(), var artifacts: List<Artifact> = ArrayList(),
var prerelease: Boolean = false, var prerelease: Boolean = false,
var latest: Boolean = false var latest: Boolean = false,
var publishTime: LocalDateTime = LocalDateTime.now(),
) { ) {
companion object { companion object {
fun fromJsonArray(jsonString: String, useTagName: Boolean): ArrayList<Release> { fun fromJsonArray(
jsonString: String, useTagName: Boolean, sortMode: SortMode
): ArrayList<Release> {
val mapper = jacksonObjectMapper() val mapper = jacksonObjectMapper()
try { try {
@ -256,39 +264,55 @@ class DriverFetcherFragment : Fragment() {
} }
releases.add(release) releases.add(release)
println(release.publishTime)
} }
} }
when (sortMode) {
SortMode.PublishTime -> releases.sortByDescending {
it.publishTime
}
else -> {}
}
return releases return releases
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
return ArrayList<Release>() return ArrayList()
} }
} }
fun fromJson(node: JsonNode, useTagName: Boolean): Release { private fun fromJson(node: JsonNode, useTagName: Boolean): Release {
try { try {
val tagName = node.get("tag_name").toString().removeSurrounding("\"") val tagName = node.get("tag_name").toString().removeSurrounding("\"")
val body = node.get("body").toString().removeSurrounding("\"") val body = node.get("body").toString().removeSurrounding("\"")
val prerelease = node.get("prerelease").toString().toBoolean() val prerelease = node.get("prerelease").toString().toBoolean()
val title = if (useTagName) tagName else node.get("name").toString().removeSurrounding("\"") val titleName = node.get("name").toString().removeSurrounding("\"")
val published = node.get("published_at").toString().removeSurrounding("\"")
val instantTime: Instant? = Instant.parse(published)
val localTime = instantTime?.atZone(ZoneId.systemDefault())?.toLocalDateTime() ?: LocalDateTime.now()
val title = if (useTagName) tagName else titleName
val assets = node.get("assets") val assets = node.get("assets")
val artifacts = ArrayList<Artifact>() val artifacts = ArrayList<Artifact>()
if (assets?.isArray == true) { if (assets?.isArray == true) {
assets.forEach { node -> assets.forEach { subNode ->
val urlStr = val urlStr = subNode.get("browser_download_url").toString()
node.get("browser_download_url").toString().removeSurrounding("\"") .removeSurrounding("\"")
val url = URL(urlStr) val url = URL(urlStr)
val name = node.get("name").toString().removeSurrounding("\"") val name = subNode.get("name").toString().removeSurrounding("\"")
val artifact = Artifact(url, name) val artifact = Artifact(url, name)
artifacts.add(artifact) artifacts.add(artifact)
} }
} }
return Release(tagName, title, body, artifacts, prerelease) return Release(tagName, titleName, title, body, artifacts, prerelease, false, localTime)
} catch (e: Exception) { } catch (e: Exception) {
// TODO: handle malformed input. // TODO: handle malformed input.
e.printStackTrace() e.printStackTrace()

View file

@ -335,7 +335,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
homeViewModel.setCheckKeys(true) homeViewModel.setCheckKeys(true)
homeViewModel.setCheckFirmware(true)
val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
if (!firstTimeSetup) {
homeViewModel.setCheckFirmware(true)
}
gamesViewModel.reloadGames(true) gamesViewModel.reloadGames(true)
return true return true
} else { } else {