Browse Source
Driver Fetcher (#130)
Driver Fetcher (#130)
Lets the user download & install drivers from various repositories. Co-authored-by: swurl <swurl@swurl.xyz> Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/130 Co-authored-by: Aleksandr Popovich <alekpopo@pm.me> Co-committed-by: Aleksandr Popovich <alekpopo@pm.me>pull/21/head
committed by
crueter
45 changed files with 1131 additions and 254 deletions
-
3src/android/app/build.gradle.kts
-
85src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/DriverGroupAdapter.kt
-
267src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/ReleaseAdapter.kt
-
19src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/SpacingItemDecoration.kt
-
1src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
-
307src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt
-
40src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
-
2src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
-
2src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
-
3src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
-
7src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
-
5src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
-
24src/android/app/src/main/jni/native.cpp
-
9src/android/app/src/main/res/drawable/ic_dropdown_arrow.xml
-
14src/android/app/src/main/res/drawable/item_release_box.xml
-
11src/android/app/src/main/res/drawable/item_release_latest_badge_background.xml
-
34src/android/app/src/main/res/layout/dialog_progress.xml
-
116src/android/app/src/main/res/layout/fragment_driver_fetcher.xml
-
11src/android/app/src/main/res/layout/fragment_driver_manager.xml
-
39src/android/app/src/main/res/layout/item_driver_group.xml
-
82src/android/app/src/main/res/layout/item_release.xml
-
9src/android/app/src/main/res/navigation/home_navigation.xml
-
11src/android/app/src/main/res/values-ar/strings.xml
-
11src/android/app/src/main/res/values-ckb/strings.xml
-
4src/android/app/src/main/res/values-cs/strings.xml
-
11src/android/app/src/main/res/values-de/strings.xml
-
11src/android/app/src/main/res/values-es/strings.xml
-
11src/android/app/src/main/res/values-fa/strings.xml
-
11src/android/app/src/main/res/values-fr/strings.xml
-
11src/android/app/src/main/res/values-he/strings.xml
-
10src/android/app/src/main/res/values-hu/strings.xml
-
11src/android/app/src/main/res/values-id/strings.xml
-
11src/android/app/src/main/res/values-it/strings.xml
-
11src/android/app/src/main/res/values-ja/strings.xml
-
11src/android/app/src/main/res/values-ko/strings.xml
-
11src/android/app/src/main/res/values-nb/strings.xml
-
11src/android/app/src/main/res/values-pl/strings.xml
-
11src/android/app/src/main/res/values-pt-rBR/strings.xml
-
11src/android/app/src/main/res/values-pt-rPT/strings.xml
-
11src/android/app/src/main/res/values-ru/strings.xml
-
11src/android/app/src/main/res/values-uk/strings.xml
-
11src/android/app/src/main/res/values-vi/strings.xml
-
11src/android/app/src/main/res/values-zh-rCN/strings.xml
-
11src/android/app/src/main/res/values-zh-rTW/strings.xml
-
61src/android/app/src/main/res/values/strings.xml
@ -0,0 +1,85 @@ |
|||
package org.yuzu.yuzu_emu.features.fetcher |
|||
|
|||
import android.annotation.SuppressLint |
|||
import android.view.LayoutInflater |
|||
import android.view.View |
|||
import android.view.ViewGroup |
|||
import androidx.recyclerview.widget.LinearLayoutManager |
|||
import androidx.recyclerview.widget.RecyclerView |
|||
import org.yuzu.yuzu_emu.databinding.ItemDriverGroupBinding |
|||
import org.yuzu.yuzu_emu.fragments.DriverFetcherFragment.DriverGroup |
|||
import androidx.core.view.isVisible |
|||
import androidx.fragment.app.FragmentActivity |
|||
import androidx.transition.AutoTransition |
|||
import androidx.transition.ChangeBounds |
|||
import androidx.transition.Fade |
|||
import androidx.transition.TransitionManager |
|||
import androidx.transition.TransitionSet |
|||
import kotlinx.coroutines.CoroutineScope |
|||
import kotlinx.coroutines.Dispatchers |
|||
import kotlinx.coroutines.launch |
|||
import org.yuzu.yuzu_emu.model.DriverViewModel |
|||
|
|||
class DriverGroupAdapter( |
|||
private val activity: FragmentActivity, |
|||
private val driverViewModel: DriverViewModel |
|||
) : RecyclerView.Adapter<DriverGroupAdapter.DriverGroupViewHolder>() { |
|||
private var driverGroups: List<DriverGroup> = emptyList() |
|||
|
|||
inner class DriverGroupViewHolder( |
|||
private val binding: ItemDriverGroupBinding |
|||
) : RecyclerView.ViewHolder(binding.root) { |
|||
fun bind(group: DriverGroup) { |
|||
val onClick = { |
|||
TransitionManager.beginDelayedTransition( |
|||
binding.root, |
|||
TransitionSet().addTransition(Fade()).addTransition(ChangeBounds()) |
|||
.setDuration(200) |
|||
) |
|||
val isVisible = binding.recyclerReleases.isVisible |
|||
|
|||
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() } |
|||
} |
|||
} |
|||
|
|||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverGroupViewHolder { |
|||
val binding = ItemDriverGroupBinding.inflate( |
|||
LayoutInflater.from(parent.context), parent, false |
|||
) |
|||
return DriverGroupViewHolder(binding) |
|||
} |
|||
|
|||
override fun onBindViewHolder(holder: DriverGroupViewHolder, position: Int) { |
|||
holder.bind(driverGroups[position]) |
|||
} |
|||
|
|||
override fun getItemCount(): Int = driverGroups.size |
|||
|
|||
@SuppressLint("NotifyDataSetChanged") |
|||
fun updateDriverGroups(newDriverGroups: List<DriverGroup>) { |
|||
driverGroups = newDriverGroups |
|||
notifyDataSetChanged() |
|||
} |
|||
} |
|||
@ -0,0 +1,267 @@ |
|||
package org.yuzu.yuzu_emu.features.fetcher |
|||
|
|||
import android.animation.LayoutTransition |
|||
import android.content.res.ColorStateList |
|||
import android.text.Html |
|||
import android.text.Html.FROM_HTML_MODE_COMPACT |
|||
import android.text.TextUtils |
|||
import android.view.LayoutInflater |
|||
import android.view.ViewGroup |
|||
import android.widget.Toast |
|||
import androidx.core.view.isVisible |
|||
import androidx.fragment.app.FragmentActivity |
|||
import androidx.recyclerview.widget.RecyclerView |
|||
import com.google.android.material.button.MaterialButton |
|||
import org.yuzu.yuzu_emu.R |
|||
import org.yuzu.yuzu_emu.databinding.ItemReleaseBinding |
|||
import org.yuzu.yuzu_emu.fragments.DriverFetcherFragment.Release |
|||
import androidx.core.net.toUri |
|||
import androidx.transition.ChangeBounds |
|||
import androidx.transition.Fade |
|||
import androidx.transition.TransitionManager |
|||
import androidx.transition.TransitionSet |
|||
import com.google.android.material.color.MaterialColors |
|||
import com.google.android.material.dialog.MaterialAlertDialogBuilder |
|||
import kotlinx.coroutines.CoroutineScope |
|||
import kotlinx.coroutines.Dispatchers |
|||
import kotlinx.coroutines.launch |
|||
import kotlinx.coroutines.withContext |
|||
import okhttp3.OkHttpClient |
|||
import okhttp3.Request |
|||
import org.commonmark.parser.Parser |
|||
import org.commonmark.renderer.html.HtmlRenderer |
|||
import org.yuzu.yuzu_emu.databinding.DialogProgressBinding |
|||
import org.yuzu.yuzu_emu.model.DriverViewModel |
|||
import org.yuzu.yuzu_emu.utils.FileUtil |
|||
import org.yuzu.yuzu_emu.utils.GpuDriverHelper |
|||
import java.io.File |
|||
import java.io.FileOutputStream |
|||
import java.io.IOException |
|||
|
|||
class ReleaseAdapter( |
|||
private val releases: List<Release>, |
|||
private val activity: FragmentActivity, |
|||
private val driverViewModel: DriverViewModel |
|||
) : RecyclerView.Adapter<ReleaseAdapter.ReleaseViewHolder>() { |
|||
|
|||
inner class ReleaseViewHolder( |
|||
private val binding: ItemReleaseBinding |
|||
) : RecyclerView.ViewHolder(binding.root) { |
|||
private var isPreview: Boolean = true |
|||
private val client = OkHttpClient() |
|||
private val markdownParser = Parser.builder().build() |
|||
private val htmlRenderer = HtmlRenderer.builder().build() |
|||
|
|||
init { |
|||
binding.root.let { root -> |
|||
val layoutTransition = root.layoutTransition ?: LayoutTransition().apply { |
|||
enableTransitionType(LayoutTransition.CHANGING) |
|||
setDuration(125) |
|||
} |
|||
root.layoutTransition = layoutTransition |
|||
} |
|||
|
|||
(binding.textBody.parent as ViewGroup).isTransitionGroup = false |
|||
binding.containerDownloads.isTransitionGroup = false |
|||
} |
|||
|
|||
fun bind(release: Release) { |
|||
binding.textReleaseName.text = release.title |
|||
binding.badgeLatest.isVisible = release.latest |
|||
|
|||
// truncates to 150 chars so it does not take up too much space. |
|||
var bodyPreview = release.body.take(150) |
|||
bodyPreview = bodyPreview.replace("#", "").removeSurrounding(" "); |
|||
|
|||
val body = |
|||
bodyPreview.replace("\\r\\n", "\n").replace("\\n", "\n").replace("\n", "<br>") |
|||
|
|||
binding.textBody.text = Html.fromHtml(body, FROM_HTML_MODE_COMPACT) |
|||
|
|||
binding.textBody.setOnClickListener { |
|||
TransitionManager.beginDelayedTransition( |
|||
binding.root, |
|||
TransitionSet().addTransition(Fade()).addTransition(ChangeBounds()) |
|||
.setDuration(100) |
|||
) |
|||
|
|||
isPreview = !isPreview |
|||
if (isPreview) { |
|||
val body = bodyPreview.replace("\\r\\n", "\n").replace("\\n", "\n") |
|||
.replace("\n", "<br>") |
|||
|
|||
binding.textBody.text = Html.fromHtml(body, FROM_HTML_MODE_COMPACT) |
|||
binding.textBody.maxLines = 3 |
|||
binding.textBody.ellipsize = TextUtils.TruncateAt.END |
|||
} else { |
|||
val body = release.body.replace("\\r\\n", "\n\n").replace("\\n", "\n\n") |
|||
|
|||
try { |
|||
val doc = markdownParser.parse(body) |
|||
val html = htmlRenderer.render(doc) |
|||
binding.textBody.text = Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT) |
|||
} catch (e: Exception) { |
|||
e.printStackTrace() |
|||
binding.textBody.text = body |
|||
} |
|||
|
|||
binding.textBody.maxLines = Integer.MAX_VALUE |
|||
binding.textBody.ellipsize = null |
|||
} |
|||
} |
|||
|
|||
val onDownloadsClick = { |
|||
val isVisible = binding.containerDownloads.isVisible |
|||
TransitionManager.beginDelayedTransition( |
|||
binding.root, |
|||
TransitionSet().addTransition(Fade()).addTransition(ChangeBounds()) |
|||
.setDuration(100) |
|||
) |
|||
|
|||
binding.containerDownloads.isVisible = !isVisible |
|||
|
|||
binding.imageDownloadsArrow.rotation = if (isVisible) 0f else 180f |
|||
binding.buttonToggleDownloads.text = |
|||
if (isVisible) activity.getString(R.string.show_downloads) |
|||
else activity.getString(R.string.hide_downloads) |
|||
} |
|||
|
|||
binding.buttonToggleDownloads.setOnClickListener { |
|||
onDownloadsClick() |
|||
} |
|||
|
|||
binding.imageDownloadsArrow.setOnClickListener { |
|||
onDownloadsClick() |
|||
} |
|||
|
|||
binding.containerDownloads.removeAllViews() |
|||
|
|||
release.artifacts.forEach { artifact -> |
|||
val button = MaterialButton(binding.root.context).apply { |
|||
text = artifact.name |
|||
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_LabelLarge) |
|||
textAlignment = MaterialButton.TEXT_ALIGNMENT_VIEW_START |
|||
setBackgroundColor(context.getColor(com.google.android.material.R.color.m3_button_background_color_selector)) |
|||
setIconResource(R.drawable.ic_import) |
|||
iconTint = ColorStateList.valueOf( |
|||
MaterialColors.getColor( |
|||
this, |
|||
com.google.android.material.R.attr.colorPrimary |
|||
) |
|||
) |
|||
|
|||
elevation = 6f |
|||
layoutParams = ViewGroup.LayoutParams( |
|||
ViewGroup.LayoutParams.MATCH_PARENT, |
|||
ViewGroup.LayoutParams.WRAP_CONTENT |
|||
) |
|||
setOnClickListener { |
|||
val dialogBinding = |
|||
DialogProgressBinding.inflate(LayoutInflater.from(context)) |
|||
dialogBinding.progressBar.isIndeterminate = true |
|||
dialogBinding.title.text = context.getString(R.string.installing_driver) |
|||
dialogBinding.status.text = context.getString(R.string.downloading) |
|||
|
|||
val progressDialog = MaterialAlertDialogBuilder(context) |
|||
.setView(dialogBinding.root) |
|||
.setCancelable(false) |
|||
.create() |
|||
|
|||
progressDialog.show() |
|||
|
|||
CoroutineScope(Dispatchers.Main).launch { |
|||
try { |
|||
val request = Request.Builder() |
|||
.url(artifact.url) |
|||
.header("Accept", "application/octet-stream") |
|||
.build() |
|||
|
|||
val cacheDir = context.externalCacheDir ?: throw IOException( |
|||
context.getString(R.string.failed_cache_dir) |
|||
) |
|||
|
|||
cacheDir.mkdirs() |
|||
|
|||
val file = File(cacheDir, artifact.name) |
|||
|
|||
withContext(Dispatchers.IO) { |
|||
client.newBuilder() |
|||
.followRedirects(true) |
|||
.followSslRedirects(true) |
|||
.build() |
|||
.newCall(request).execute().use { response -> |
|||
if (!response.isSuccessful) { |
|||
throw IOException("${response.code}") |
|||
} |
|||
|
|||
response.body?.byteStream()?.use { input -> |
|||
FileOutputStream(file).use { output -> |
|||
input.copyTo(output) |
|||
} |
|||
} |
|||
?: throw IOException(context.getString(R.string.empty_response_body)) |
|||
} |
|||
} |
|||
|
|||
if (file.length() == 0L) { |
|||
throw IOException(context.getString(R.string.driver_empty)) |
|||
} |
|||
|
|||
dialogBinding.status.text = context.getString(R.string.installing) |
|||
|
|||
val driverData = GpuDriverHelper.getMetadataFromZip(file) |
|||
val driverPath = |
|||
"${GpuDriverHelper.driverStoragePath}${FileUtil.getFilename(file.toUri())}" |
|||
|
|||
if (GpuDriverHelper.copyDriverToInternalStorage(file.toUri())) { |
|||
driverViewModel.onDriverAdded(Pair(driverPath, driverData)) |
|||
|
|||
progressDialog.dismiss() |
|||
Toast.makeText( |
|||
context, |
|||
context.getString( |
|||
R.string.successfully_installed, |
|||
driverData.name |
|||
), |
|||
Toast.LENGTH_SHORT |
|||
).show() |
|||
} else { |
|||
throw IOException( |
|||
context.getString( |
|||
R.string.failed_install_driver, |
|||
artifact.name |
|||
) |
|||
) |
|||
} |
|||
} catch (e: Exception) { |
|||
progressDialog.dismiss() |
|||
|
|||
MaterialAlertDialogBuilder(context) |
|||
.setTitle(context.getString(R.string.driver_failed_title)) |
|||
.setMessage(e.message) |
|||
.setPositiveButton(R.string.ok) { dialog, _ -> |
|||
dialog.cancel() |
|||
} |
|||
.show() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
binding.containerDownloads.addView(button) |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReleaseViewHolder { |
|||
val binding = ItemReleaseBinding.inflate( |
|||
LayoutInflater.from(parent.context), parent, false |
|||
) |
|||
return ReleaseViewHolder(binding) |
|||
} |
|||
|
|||
override fun onBindViewHolder(holder: ReleaseViewHolder, position: Int) { |
|||
holder.bind(releases[position]) |
|||
} |
|||
|
|||
override fun getItemCount(): Int = releases.size |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
package org.yuzu.yuzu_emu.features.fetcher |
|||
|
|||
import android.graphics.Rect |
|||
import android.view.View |
|||
import androidx.recyclerview.widget.RecyclerView |
|||
|
|||
class SpacingItemDecoration(private val spacing: Int) : RecyclerView.ItemDecoration() { |
|||
override fun getItemOffsets( |
|||
outRect: Rect, |
|||
view: View, |
|||
parent: RecyclerView, |
|||
state: RecyclerView.State |
|||
) { |
|||
outRect.bottom = spacing |
|||
if (parent.getChildAdapterPosition(view) == 0) { |
|||
outRect.top = spacing |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,307 @@ |
|||
package org.yuzu.yuzu_emu.fragments |
|||
|
|||
import android.app.AlertDialog |
|||
import android.os.Bundle |
|||
import androidx.fragment.app.Fragment |
|||
import android.view.LayoutInflater |
|||
import android.view.View |
|||
import android.view.ViewGroup |
|||
import androidx.core.view.ViewCompat |
|||
import androidx.core.view.WindowInsetsCompat |
|||
import androidx.core.view.isVisible |
|||
import androidx.core.view.updatePadding |
|||
import androidx.fragment.app.activityViewModels |
|||
import androidx.navigation.findNavController |
|||
import androidx.recyclerview.widget.LinearLayoutManager |
|||
import com.fasterxml.jackson.databind.JsonNode |
|||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper |
|||
import com.google.android.material.dialog.MaterialAlertDialogBuilder |
|||
import com.google.android.material.transition.MaterialSharedAxis |
|||
import kotlinx.coroutines.CoroutineScope |
|||
import kotlinx.coroutines.Dispatchers |
|||
import kotlinx.coroutines.launch |
|||
import kotlinx.coroutines.withContext |
|||
import okhttp3.OkHttpClient |
|||
import okhttp3.Request |
|||
import org.yuzu.yuzu_emu.R |
|||
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 kotlin.getValue |
|||
|
|||
class DriverFetcherFragment : Fragment() { |
|||
private var _binding: FragmentDriverFetcherBinding? = null |
|||
private val binding get() = _binding!! |
|||
|
|||
private val client = OkHttpClient() |
|||
|
|||
private val gpuModel: String? |
|||
get() = GpuDriverHelper.getGpuModel() |
|||
|
|||
private val adrenoModel: Int |
|||
get() = parseAdrenoModel() |
|||
|
|||
private val recommendedDriver: String |
|||
get() = driverMap.firstOrNull { adrenoModel in it.first }?.second ?: "Unsupported" |
|||
|
|||
private data class DriverRepo( |
|||
val name: String = "", |
|||
val path: String = "", |
|||
val sort: Int = 0, |
|||
val useTagName: Boolean = false |
|||
) |
|||
|
|||
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("Weab-Chan Freedreno", "Weab-chan/freedreno_turnip-CI", 3), |
|||
) |
|||
|
|||
private val driverMap = listOf( |
|||
IntRange(Integer.MIN_VALUE, 9) to "Unsupported", |
|||
IntRange(10, 99) to "KIMCHI Latest", |
|||
IntRange(100, 599) to "Unsupported", |
|||
IntRange(600, 639) to "Mr. Purple EOL-24.3.4", |
|||
IntRange(640, 699) to "Mr. Purple T19", |
|||
IntRange(700, 799) to "Mr. Purple T20", // TODO: Await T21 and update accordingly |
|||
IntRange(800, 899) to "GameHub Adreno 8xx", |
|||
IntRange(900, Int.MAX_VALUE) to "Unsupported", |
|||
) |
|||
|
|||
private lateinit var driverGroupAdapter: DriverGroupAdapter |
|||
private val driverViewModel: DriverViewModel by activityViewModels() |
|||
|
|||
fun parseAdrenoModel(): Int { |
|||
if (gpuModel == null) { |
|||
return 0 |
|||
} |
|||
|
|||
val modelList = gpuModel!!.split(" ") |
|||
|
|||
// format: Adreno (TM) <ModelNumber> |
|||
if (modelList.size < 3 || modelList[0] != "Adreno") { |
|||
return 0 |
|||
} |
|||
|
|||
val model = modelList[2] |
|||
|
|||
try { |
|||
// special case for Axx GPUs (e.g. AYANEO Pocket S2) |
|||
// driverMap has specific ranges for this |
|||
// needs to be fixed |
|||
if (model.startsWith("A")) { |
|||
return model.substring(1).toInt() |
|||
} |
|||
|
|||
return model.toInt() |
|||
} catch (e: Exception) { |
|||
// Model parse error, just say unsupported |
|||
e.printStackTrace() |
|||
return 0 |
|||
} |
|||
} |
|||
|
|||
override fun onCreate(savedInstanceState: Bundle?) { |
|||
super.onCreate(savedInstanceState) |
|||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) |
|||
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) |
|||
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) |
|||
} |
|||
|
|||
override fun onCreateView( |
|||
inflater: LayoutInflater, container: ViewGroup?, |
|||
savedInstanceState: Bundle? |
|||
): View? { |
|||
_binding = FragmentDriverFetcherBinding.inflate(inflater) |
|||
binding.badgeRecommendedDriver.text = recommendedDriver |
|||
binding.badgeGpuModel.text = gpuModel |
|||
|
|||
return binding.root |
|||
} |
|||
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
|||
super.onViewCreated(view, savedInstanceState) |
|||
|
|||
binding.toolbarDrivers.setNavigationOnClickListener { |
|||
binding.root.findNavController().popBackStack() |
|||
} |
|||
|
|||
binding.listDrivers.layoutManager = LinearLayoutManager(context) |
|||
driverGroupAdapter = DriverGroupAdapter(requireActivity(), driverViewModel) |
|||
binding.listDrivers.adapter = driverGroupAdapter |
|||
|
|||
setInsets() |
|||
|
|||
fetchDrivers() |
|||
} |
|||
|
|||
private fun fetchDrivers() { |
|||
binding.loadingIndicator.isVisible = true |
|||
|
|||
val driverGroups = arrayListOf<DriverGroup>() |
|||
|
|||
repoList.forEach { driver -> |
|||
val name = driver.name |
|||
val path = driver.path |
|||
val useTagName = driver.useTagName |
|||
val sort = driver.sort |
|||
CoroutineScope(Dispatchers.Main).launch { |
|||
val request = Request.Builder() |
|||
.url("https://api.github.com/repos/$path/releases") |
|||
.build() |
|||
|
|||
withContext(Dispatchers.IO) { |
|||
var releases: ArrayList<Release> |
|||
try { |
|||
client.newCall(request).execute().use { response -> |
|||
if (!response.isSuccessful) { |
|||
throw IOException(response.body.toString()) |
|||
} |
|||
|
|||
val body = response.body?.string() ?: return@withContext |
|||
releases = Release.fromJsonArray(body, useTagName) |
|||
} |
|||
} catch (e: Exception) { |
|||
withContext(Dispatchers.Main) { |
|||
MaterialAlertDialogBuilder(requireActivity().applicationContext) |
|||
.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>() |
|||
} |
|||
} |
|||
|
|||
val driver = DriverGroup( |
|||
name, |
|||
releases, |
|||
sort |
|||
) |
|||
|
|||
synchronized(driverGroups) { |
|||
driverGroups.add(driver) |
|||
driverGroups.sortBy { |
|||
it.sort |
|||
} |
|||
} |
|||
|
|||
withContext(Dispatchers.Main) { |
|||
driverGroupAdapter.updateDriverGroups(driverGroups) |
|||
|
|||
if (driverGroups.size >= repoList.size) { |
|||
binding.loadingIndicator.isVisible = false |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
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 |
|||
|
|||
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) |
|||
) |
|||
|
|||
windowInsets |
|||
} |
|||
|
|||
data class Artifact(val url: URL, val name: String) |
|||
|
|||
data class Release( |
|||
var tagName: String = "", |
|||
var title: String = "", |
|||
var body: String = "", |
|||
var artifacts: List<Artifact> = ArrayList<Artifact>(), |
|||
var prerelease: Boolean = false, |
|||
var latest: Boolean = false |
|||
) { |
|||
companion object { |
|||
fun fromJsonArray(jsonString: String, useTagName: Boolean): ArrayList<Release> { |
|||
val mapper = jacksonObjectMapper() |
|||
|
|||
try { |
|||
val rootNode = mapper.readTree(jsonString) |
|||
|
|||
val releases = ArrayList<Release>() |
|||
|
|||
var latestRelease: Release? = null |
|||
|
|||
if (rootNode.isArray) { |
|||
rootNode.forEach { node -> |
|||
val release = fromJson(node, useTagName) |
|||
|
|||
if (latestRelease == null && !release.prerelease) { |
|||
latestRelease = release |
|||
release.latest = true |
|||
} |
|||
|
|||
releases.add(release) |
|||
} |
|||
} |
|||
|
|||
return releases |
|||
} catch (e: Exception) { |
|||
e.printStackTrace() |
|||
return ArrayList<Release>() |
|||
} |
|||
} |
|||
|
|||
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 assets = node.get("assets") |
|||
val artifacts = ArrayList<Artifact>() |
|||
if (assets?.isArray == true) { |
|||
assets.forEach { node -> |
|||
val urlStr = |
|||
node.get("browser_download_url").toString().removeSurrounding("\"") |
|||
|
|||
val url = URL(urlStr) |
|||
val name = node.get("name").toString().removeSurrounding("\"") |
|||
|
|||
val artifact = Artifact(url, name) |
|||
artifacts.add(artifact) |
|||
} |
|||
} |
|||
|
|||
return Release(tagName, title, body, artifacts, prerelease) |
|||
} catch (e: Exception) { |
|||
// TODO: handle malformed input. |
|||
e.printStackTrace() |
|||
} |
|||
|
|||
return Release() |
|||
} |
|||
} |
|||
} |
|||
|
|||
data class DriverGroup( |
|||
val name: String, |
|||
val releases: ArrayList<Release>, |
|||
val sort: Int, |
|||
) |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:width="24dp" |
|||
android:height="24dp" |
|||
android:viewportWidth="24" |
|||
android:viewportHeight="24"> |
|||
<path |
|||
android:fillColor="#FF000000" |
|||
android:pathData="M7,10l5,5 5,-5" /> |
|||
</vector> |
|||
@ -0,0 +1,14 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<shape xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:shape="rectangle"> |
|||
<solid android:color="?attr/colorSurface" /> |
|||
<stroke |
|||
android:width="1dp" |
|||
android:color="?attr/colorOutline" /> |
|||
<corners android:radius="8dp" /> |
|||
<padding |
|||
android:left="12dp" |
|||
android:top="12dp" |
|||
android:right="12dp" |
|||
android:bottom="12dp" /> |
|||
</shape> |
|||
@ -0,0 +1,11 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<shape xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:shape="rectangle"> |
|||
<solid android:color="?attr/colorSurfaceVariant" /> |
|||
<corners android:radius="8dp" /> |
|||
<padding |
|||
android:left="8dp" |
|||
android:right="8dp" |
|||
android:top="0dp" |
|||
android:bottom="0dp" /> |
|||
</shape> |
|||
@ -0,0 +1,34 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:id="@+id/dialog_progress_layout" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:orientation="vertical" |
|||
android:padding="16dp"> |
|||
|
|||
<TextView |
|||
android:id="@+id/title" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginBottom="8dp" |
|||
android:textAlignment="center" |
|||
android:textAppearance="?attr/textAppearanceHeadline6" |
|||
android:textColor="?attr/colorPrimary" /> |
|||
|
|||
<ProgressBar |
|||
android:id="@+id/progress_bar" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:layout_gravity="center" |
|||
android:indeterminate="true" |
|||
android:layout_marginBottom="8dp" /> |
|||
|
|||
<TextView |
|||
android:id="@+id/status" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:textAlignment="center" |
|||
android:textAppearance="?attr/textAppearanceBody1" |
|||
android:textColor="?attr/colorOnSurface" /> |
|||
|
|||
</LinearLayout> |
|||
@ -0,0 +1,116 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|||
xmlns:app="http://schemas.android.com/apk/res-auto" |
|||
xmlns:tools="http://schemas.android.com/tools" |
|||
android:id="@+id/coordinator_licenses" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="match_parent" |
|||
android:background="?attr/colorSurface"> |
|||
|
|||
|
|||
<com.google.android.material.appbar.AppBarLayout |
|||
android:id="@+id/appbar_drivers" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:fitsSystemWindows="true" |
|||
android:touchscreenBlocksFocus="false" |
|||
app:layout_constraintTop_toTopOf="parent" |
|||
|
|||
app:liftOnScrollTargetViewId="@id/list_drivers"> |
|||
|
|||
<com.google.android.material.appbar.MaterialToolbar |
|||
android:id="@+id/toolbar_drivers" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="?attr/actionBarSize" |
|||
android:touchscreenBlocksFocus="false" |
|||
app:navigationIcon="@drawable/ic_back" |
|||
app:title="@string/gpu_driver_fetcher" /> |
|||
|
|||
</com.google.android.material.appbar.AppBarLayout> |
|||
|
|||
<TextView |
|||
android:id="@+id/label_gpu_model" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginStart="8dp" |
|||
android:layout_marginTop="8dp" |
|||
android:padding="8dp" |
|||
android:text="@string/gpu_model" |
|||
android:textSize="16sp" |
|||
android:textStyle="bold" |
|||
app:layout_behavior="@string/appbar_scrolling_view_behavior" |
|||
app:layout_constraintStart_toStartOf="parent" |
|||
app:layout_constraintTop_toBottomOf="@id/appbar_drivers" /> |
|||
|
|||
<TextView |
|||
android:id="@+id/badge_gpu_model" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginStart="8dp" |
|||
android:background="@drawable/item_release_latest_badge_background" |
|||
android:textColor="?attr/colorPrimary" |
|||
android:textSize="16sp" |
|||
app:layout_constraintBottom_toBottomOf="@id/label_gpu_model" |
|||
app:layout_constraintStart_toEndOf="@id/label_gpu_model" |
|||
app:layout_constraintTop_toTopOf="@id/label_gpu_model" /> |
|||
|
|||
<TextView |
|||
android:id="@+id/label_recommended_driver" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginStart="8dp" |
|||
android:layout_marginTop="8dp" |
|||
android:padding="8dp" |
|||
android:text="@string/recommended_driver" |
|||
android:textSize="16sp" |
|||
android:textStyle="bold" |
|||
app:layout_behavior="@string/appbar_scrolling_view_behavior" |
|||
app:layout_constraintStart_toStartOf="parent" |
|||
app:layout_constraintTop_toBottomOf="@id/label_gpu_model" /> |
|||
|
|||
<TextView |
|||
android:id="@+id/badge_recommended_driver" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginStart="8dp" |
|||
android:background="@drawable/item_release_latest_badge_background" |
|||
android:textColor="?attr/colorPrimary" |
|||
android:textSize="16sp" |
|||
app:layout_constraintBottom_toBottomOf="@id/label_recommended_driver" |
|||
app:layout_constraintStart_toEndOf="@id/label_recommended_driver" |
|||
app:layout_constraintTop_toTopOf="@id/label_recommended_driver" /> |
|||
|
|||
<View |
|||
android:id="@+id/divider" |
|||
android:layout_width="0dp" |
|||
android:layout_height="1dp" |
|||
android:layout_marginStart="16dp" |
|||
android:layout_marginTop="8dp" |
|||
android:layout_marginEnd="16dp" |
|||
android:background="?attr/colorOutline" |
|||
app:layout_constraintEnd_toEndOf="parent" |
|||
app:layout_constraintStart_toStartOf="parent" |
|||
app:layout_constraintTop_toBottomOf="@id/label_recommended_driver" /> |
|||
|
|||
<androidx.recyclerview.widget.RecyclerView |
|||
android:id="@+id/list_drivers" |
|||
android:layout_height="0dp" |
|||
android:layout_width="match_parent" |
|||
android:layout_marginTop="8dp" |
|||
android:clipToPadding="false" |
|||
app:layout_behavior="@string/appbar_scrolling_view_behavior" |
|||
app:layout_constraintTop_toBottomOf="@id/divider" |
|||
app:layout_constraintBottom_toBottomOf="parent" /> |
|||
|
|||
<ProgressBar |
|||
android:id="@+id/loadingIndicator" |
|||
style="?android:attr/progressBarStyle" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginTop="12dp" |
|||
android:visibility="invisible" |
|||
app:layout_constraintEnd_toEndOf="@id/list_drivers" |
|||
app:layout_constraintStart_toStartOf="@id/list_drivers" |
|||
app:layout_constraintTop_toTopOf="@id/list_drivers" /> |
|||
|
|||
</androidx.constraintlayout.widget.ConstraintLayout> |
|||
@ -0,0 +1,39 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<androidx.constraintlayout.widget.ConstraintLayout |
|||
xmlns:android="http://schemas.android.com/apk/res/android" |
|||
xmlns:app="http://schemas.android.com/apk/res-auto" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:padding="16dp"> |
|||
|
|||
<TextView |
|||
android:id="@+id/text_group_name" |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:textSize="18sp" |
|||
android:textStyle="bold" |
|||
android:textColor="?attr/colorControlNormal" |
|||
app:layout_constraintStart_toStartOf="parent" |
|||
app:layout_constraintTop_toTopOf="parent" |
|||
app:layout_constraintEnd_toStartOf="@id/image_dropdown_arrow" /> |
|||
|
|||
<ImageView |
|||
android:id="@+id/image_dropdown_arrow" |
|||
android:layout_width="32dp" |
|||
android:layout_height="24dp" |
|||
android:src="@drawable/ic_dropdown_arrow" |
|||
app:tint="?attr/colorControlNormal" |
|||
app:layout_constraintEnd_toEndOf="parent" |
|||
app:layout_constraintTop_toTopOf="parent" |
|||
android:contentDescription="@string/show_releases" /> |
|||
|
|||
<androidx.recyclerview.widget.RecyclerView |
|||
android:id="@+id/recycler_releases" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:visibility="gone" |
|||
app:layout_constraintTop_toBottomOf="@id/text_group_name" |
|||
app:layout_constraintStart_toStartOf="parent" |
|||
android:layout_marginTop="8dp" /> |
|||
|
|||
</androidx.constraintlayout.widget.ConstraintLayout> |
|||
@ -0,0 +1,82 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<androidx.constraintlayout.widget.ConstraintLayout |
|||
xmlns:android="http://schemas.android.com/apk/res/android" |
|||
xmlns:app="http://schemas.android.com/apk/res-auto" |
|||
xmlns:tools="http://schemas.android.com/tools" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:background="@drawable/item_release_box" |
|||
android:padding="8dp" |
|||
android:paddingStart="12dp" |
|||
tools:ignore="RtlSymmetry"> |
|||
|
|||
<TextView |
|||
android:id="@+id/text_release_name" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:textSize="16sp" |
|||
android:textStyle="bold" |
|||
app:layout_constraintStart_toStartOf="parent" |
|||
app:layout_constraintTop_toTopOf="parent" /> |
|||
|
|||
<TextView |
|||
android:id="@+id/badge_latest" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:text="@string/latest" |
|||
android:textColor="?attr/colorPrimary" |
|||
android:textSize="14sp" |
|||
android:background="@drawable/item_release_latest_badge_background" |
|||
app:layout_constraintStart_toEndOf="@id/text_release_name" |
|||
app:layout_constraintTop_toTopOf="@id/text_release_name" |
|||
app:layout_constraintBottom_toBottomOf="@id/text_release_name" |
|||
android:layout_marginStart="8dp" /> |
|||
|
|||
<TextView |
|||
android:id="@+id/text_body" |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginStart="8dp" |
|||
android:layout_marginTop="4dp" |
|||
android:ellipsize="end" |
|||
android:maxLines="3" |
|||
android:textSize="14sp" |
|||
app:layout_constraintEnd_toEndOf="parent" |
|||
app:layout_constraintStart_toStartOf="parent" |
|||
app:layout_constraintTop_toBottomOf="@id/text_release_name" /> |
|||
|
|||
<TextView |
|||
android:id="@+id/button_toggle_downloads" |
|||
android:layout_width="wrap_content" |
|||
android:layout_height="wrap_content" |
|||
android:text="@string/show_downloads" |
|||
android:textStyle="bold" |
|||
android:textSize="16sp" |
|||
app:layout_constraintStart_toStartOf="parent" |
|||
app:layout_constraintTop_toBottomOf="@id/text_body" |
|||
android:layout_marginTop="8dp" |
|||
android:layout_marginStart="8dp"/> |
|||
|
|||
<ImageView |
|||
android:id="@+id/image_downloads_arrow" |
|||
android:layout_width="24dp" |
|||
android:layout_height="24dp" |
|||
android:layout_marginStart="8dp" |
|||
android:contentDescription="@string/show_downloads" |
|||
android:src="@drawable/ic_dropdown_arrow" |
|||
app:layout_constraintBottom_toBottomOf="@id/button_toggle_downloads" |
|||
app:layout_constraintStart_toEndOf="@id/button_toggle_downloads" |
|||
app:layout_constraintTop_toTopOf="@id/button_toggle_downloads" |
|||
app:tint="?attr/colorControlNormal" /> |
|||
|
|||
<LinearLayout |
|||
android:id="@+id/container_downloads" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:orientation="vertical" |
|||
android:visibility="gone" |
|||
app:layout_constraintTop_toBottomOf="@id/button_toggle_downloads" |
|||
app:layout_constraintStart_toStartOf="parent" |
|||
android:layout_marginTop="8dp" /> |
|||
|
|||
</androidx.constraintlayout.widget.ConstraintLayout> |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue