mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2025-07-20 12:55:45 +00:00
[android] improve driver fetcher (#251)
- 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:
parent
aeb2aec13b
commit
b60d0aabf0
3 changed files with 118 additions and 65 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue