[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
import android.annotation.SuppressLint
@ -17,7 +20,9 @@ import androidx.transition.TransitionManager
import androidx.transition.TransitionSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.yuzu.yuzu_emu.model.DriverViewModel
class DriverGroupAdapter(
@ -25,43 +30,61 @@ class DriverGroupAdapter(
private val driverViewModel: DriverViewModel
) : RecyclerView.Adapter<DriverGroupAdapter.DriverGroupViewHolder>() {
private var driverGroups: List<DriverGroup> = emptyList()
private val adapterJobs = mutableMapOf<Int, Job>()
inner class DriverGroupViewHolder(
private val binding: ItemDriverGroupBinding
) : RecyclerView.ViewHolder(binding.root) {
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 = {
adapterJobs[bindingAdapterPosition]?.cancel()
TransitionManager.beginDelayedTransition(
binding.root,
TransitionSet().addTransition(Fade()).addTransition(ChangeBounds())
.setDuration(200)
)
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.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.imageDropdownArrow.setOnClickListener { onClick() }
}
fun clear() {
adapterJobs[bindingAdapterPosition]?.cancel()
adapterJobs.remove(bindingAdapterPosition)
}
}
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
import android.app.AlertDialog
import android.os.Bundle
import androidx.fragment.app.Fragment
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.model.DriverViewModel
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import java.io.IOException
import java.net.URL
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import kotlin.getValue
class DriverFetcherFragment : Fragment() {
@ -49,17 +53,22 @@ class DriverFetcherFragment : Fragment() {
private val recommendedDriver: String
get() = driverMap.firstOrNull { adrenoModel in it.first }?.second ?: "Unsupported"
enum class SortMode {
Default, PublishTime,
}
private data class DriverRepo(
val name: String = "",
val path: String = "",
val sort: Int = 0,
val useTagName: Boolean = false
val useTagName: Boolean = false,
val sortMode: SortMode = SortMode.Default,
)
private val repoList: List<DriverRepo> = listOf(
DriverRepo("Mr. Purple Turnip", "MrPurple666/purple-turnip", 0),
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),
)
@ -78,7 +87,7 @@ class DriverFetcherFragment : Fragment() {
private lateinit var driverGroupAdapter: DriverGroupAdapter
private val driverViewModel: DriverViewModel by activityViewModels()
fun parseAdrenoModel(): Int {
private fun parseAdrenoModel(): Int {
if (gpuModel == null) {
return 0
}
@ -115,9 +124,8 @@ class DriverFetcherFragment : Fragment() {
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = FragmentDriverFetcherBinding.inflate(inflater)
binding.badgeRecommendedDriver.text = recommendedDriver
binding.badgeGpuModel.text = gpuModel
@ -150,11 +158,12 @@ class DriverFetcherFragment : Fragment() {
val name = driver.name
val path = driver.path
val useTagName = driver.useTagName
val sortMode = driver.sortMode
val sort = driver.sort
CoroutineScope(Dispatchers.Main).launch {
val request = Request.Builder()
.url("https://api.github.com/repos/$path/releases")
.build()
val request =
Request.Builder().url("https://api.github.com/repos/$path/releases").build()
withContext(Dispatchers.IO) {
var releases: ArrayList<Release>
@ -165,28 +174,25 @@ class DriverFetcherFragment : Fragment() {
}
val body = response.body?.string() ?: return@withContext
releases = Release.fromJsonArray(body, useTagName)
releases = Release.fromJsonArray(body, useTagName, sortMode)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
MaterialAlertDialogBuilder(requireActivity().applicationContext)
.setTitle(getString(R.string.error_during_fetch))
MaterialAlertDialogBuilder(requireActivity()).setTitle(getString(R.string.error_during_fetch))
.setMessage("${getString(R.string.failed_to_fetch)} ${name}:\n${e.message}")
.setPositiveButton(getString(R.string.ok)) { dialog, _ -> dialog.cancel() }
.show()
releases = ArrayList<Release>()
releases = ArrayList()
}
}
val driver = DriverGroup(
name,
releases,
sort
val group = DriverGroup(
name, releases, sort
)
synchronized(driverGroups) {
driverGroups.add(driver)
driverGroups.add(group)
driverGroups.sortBy {
it.sort
}
@ -204,39 +210,41 @@ class DriverFetcherFragment : Fragment() {
}
}
private fun setInsets() =
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { _: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
private fun setInsets() = ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { _: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val leftInsets = barInsets.left + cutoutInsets.left
val rightInsets = barInsets.right + cutoutInsets.right
val leftInsets = barInsets.left + cutoutInsets.left
val rightInsets = barInsets.right + cutoutInsets.right
binding.toolbarDrivers.updateMargins(left = leftInsets, right = rightInsets)
binding.listDrivers.updateMargins(left = leftInsets, right = rightInsets)
binding.toolbarDrivers.updateMargins(left = leftInsets, right = rightInsets)
binding.listDrivers.updateMargins(left = leftInsets, right = rightInsets)
binding.listDrivers.updatePadding(
bottom = barInsets.bottom +
resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
)
binding.listDrivers.updatePadding(
bottom = barInsets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
)
windowInsets
}
windowInsets
}
data class Artifact(val url: URL, val name: String)
data class Release(
var tagName: String = "",
var titleName: String = "",
var title: String = "",
var body: String = "",
var artifacts: List<Artifact> = ArrayList<Artifact>(),
var artifacts: List<Artifact> = ArrayList(),
var prerelease: Boolean = false,
var latest: Boolean = false
var latest: Boolean = false,
var publishTime: LocalDateTime = LocalDateTime.now(),
) {
companion object {
fun fromJsonArray(jsonString: String, useTagName: Boolean): ArrayList<Release> {
fun fromJsonArray(
jsonString: String, useTagName: Boolean, sortMode: SortMode
): ArrayList<Release> {
val mapper = jacksonObjectMapper()
try {
@ -256,39 +264,55 @@ class DriverFetcherFragment : Fragment() {
}
releases.add(release)
println(release.publishTime)
}
}
when (sortMode) {
SortMode.PublishTime -> releases.sortByDescending {
it.publishTime
}
else -> {}
}
return releases
} catch (e: Exception) {
e.printStackTrace()
return ArrayList<Release>()
return ArrayList()
}
}
fun fromJson(node: JsonNode, useTagName: Boolean): Release {
private fun fromJson(node: JsonNode, useTagName: Boolean): Release {
try {
val tagName = node.get("tag_name").toString().removeSurrounding("\"")
val body = node.get("body").toString().removeSurrounding("\"")
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 artifacts = ArrayList<Artifact>()
if (assets?.isArray == true) {
assets.forEach { node ->
val urlStr =
node.get("browser_download_url").toString().removeSurrounding("\"")
assets.forEach { subNode ->
val urlStr = subNode.get("browser_download_url").toString()
.removeSurrounding("\"")
val url = URL(urlStr)
val name = node.get("name").toString().removeSurrounding("\"")
val name = subNode.get("name").toString().removeSurrounding("\"")
val artifact = Artifact(url, name)
artifacts.add(artifact)
}
}
return Release(tagName, title, body, artifacts, prerelease)
return Release(tagName, titleName, title, body, artifacts, prerelease, false, localTime)
} catch (e: Exception) {
// TODO: handle malformed input.
e.printStackTrace()

View file

@ -335,7 +335,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
Toast.LENGTH_SHORT
).show()
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)
return true
} else {