From f9fc9fa5ed18a4355707d3342554daa8f9e39772 Mon Sep 17 00:00:00 2001 From: Ishan09811 <156402647+Ishan09811@users.noreply.github.com> Date: Sat, 1 Feb 2025 21:11:27 +0530 Subject: [PATCH] Implement GpuDriver import option (#66) --- app/build.gradle | 8 ++ .../input/onscreen/OnScreenItemDefinitions.kt | 13 ++- .../skyline/preference/GpuDriverActivity.kt | 103 ++++++++++++++++- .../java/emu/skyline/utils/DriversFetcher.kt | 105 ++++++++++++++++++ app/src/main/res/layout/dialog_keyboard.xml | 24 ++++ app/src/main/res/values/strings.xml | 6 + build.gradle | 2 +- 7 files changed, 249 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/emu/skyline/utils/DriversFetcher.kt create mode 100644 app/src/main/res/layout/dialog_keyboard.xml diff --git a/app/build.gradle b/app/build.gradle index 30738952..ec790efc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -193,6 +193,14 @@ dependencies { implementation 'info.debatty:java-string-similarity:2.0.0' implementation 'com.github.KikiManjaro:colorpicker:v1.1.12' implementation 'com.github.android:renderscript-intrinsics-replacement-toolkit:344be3f' + + /* Network */ + implementation 'io.ktor:ktor-client-core:3.0.3' + implementation 'io.ktor:ktor-client-cio:3.0.3' + implementation 'io.ktor:ktor-client-json:3.0.3' + implementation 'io.ktor:ktor-serialization-kotlinx-json:3.0.3' + implementation 'io.ktor:ktor-client-content-negotiation:3.0.3' + implementation 'io.ktor:ktor-client-logging:3.0.3' } kapt { diff --git a/app/src/main/java/emu/skyline/input/onscreen/OnScreenItemDefinitions.kt b/app/src/main/java/emu/skyline/input/onscreen/OnScreenItemDefinitions.kt index 1f60169f..e9db7461 100644 --- a/app/src/main/java/emu/skyline/input/onscreen/OnScreenItemDefinitions.kt +++ b/app/src/main/java/emu/skyline/input/onscreen/OnScreenItemDefinitions.kt @@ -25,6 +25,7 @@ import emu.skyline.input.StickId.Right import emu.skyline.utils.SwitchColors import emu.skyline.utils.add import emu.skyline.utils.multiply +import emu.skyline.SkylineApplication import kotlin.math.roundToInt open class CircularButton( @@ -33,7 +34,7 @@ open class CircularButton( defaultRelativeX : Float, defaultRelativeY : Float, defaultRelativeRadiusToX : Float, - drawableId : Int = R.drawable.ic_button, + drawableId : Int = SkylineApplication.context.resources.getIdentifier("ic_button", "drawable", SkylineApplication.context.packageName), defaultEnabled : Boolean = true ) : OnScreenButton( onScreenControllerView, @@ -65,9 +66,9 @@ open class JoystickButton( defaultRelativeX, defaultRelativeY, defaultRelativeRadiusToX, - R.drawable.ic_button + SkylineApplication.context.resources.getIdentifier("ic_button", "drawable", SkylineApplication.context.packageName) ) { - private val innerButton = CircularButton(onScreenControllerView, buttonId, config.relativeX, config.relativeY, defaultRelativeRadiusToX * 0.75f, R.drawable.ic_stick) + private val innerButton = CircularButton(onScreenControllerView, buttonId, config.relativeX, config.relativeY, defaultRelativeRadiusToX * 0.75f, SkylineApplication.context.resources.getIdentifier("ic_stick", "drawable", SkylineApplication.context.packageName)) open var recenterSticks = false set(value) { @@ -275,7 +276,7 @@ open class RectangularButton( defaultRelativeY : Float, defaultRelativeWidth : Float, defaultRelativeHeight : Float, - drawableId : Int = R.drawable.ic_rectangular_button, + drawableId : Int = SkylineApplication.context.resources.getIdentifier("ic_rectangular_button", "drawable", SkylineApplication.context.packageName), defaultEnabled : Boolean = true ) : OnScreenButton( onScreenControllerView, @@ -305,9 +306,9 @@ class TriggerButton( defaultRelativeWidth, defaultRelativeHeight, when (buttonId) { - ZL -> R.drawable.ic_trigger_button_left + ZL -> SkylineApplication.context.resources.getIdentifier("ic_trigger_button_left", "drawable", SkylineApplication.context.packageName) - ZR -> R.drawable.ic_trigger_button_right + ZR -> SkylineApplication.context.resources.getIdentifier("ic_trigger_button_right", "drawable", SkylineApplication.context.packageName) else -> error("Unsupported trigger button") } diff --git a/app/src/main/java/emu/skyline/preference/GpuDriverActivity.kt b/app/src/main/java/emu/skyline/preference/GpuDriverActivity.kt index ee9a74aa..2a5ecba6 100644 --- a/app/src/main/java/emu/skyline/preference/GpuDriverActivity.kt +++ b/app/src/main/java/emu/skyline/preference/GpuDriverActivity.kt @@ -9,16 +9,19 @@ import android.content.Intent import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION import android.os.Bundle import android.view.ViewTreeObserver +import android.view.LayoutInflater import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.WindowCompat +import androidx.core.widget.doOnTextChanged import androidx.recyclerview.widget.LinearLayoutManager import androidx.viewbinding.ViewBinding import androidx.lifecycle.lifecycleScope import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.google.android.material.snackbar.Snackbar +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import emu.skyline.R import emu.skyline.adapter.GenericListItem @@ -28,17 +31,24 @@ import emu.skyline.adapter.SpacingItemDecoration import emu.skyline.data.BaseAppItem import emu.skyline.data.AppItemTag import emu.skyline.databinding.GpuDriverActivityBinding +import emu.skyline.databinding.DialogKeyboardBinding import emu.skyline.settings.EmulationSettings import emu.skyline.utils.GpuDriverHelper import emu.skyline.utils.GpuDriverInstallResult import emu.skyline.utils.WindowInsetsHelper import emu.skyline.utils.serializable +import emu.skyline.utils.DriversFetcher +import emu.skyline.utils.DriversFetcher.DownloadResult import emu.skyline.di.getSettings import emu.skyline.SkylineApplication +import emu.skyline.getPublicFilesDir import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.flow.distinctUntilChanged +import java.io.File +import java.io.FileInputStream /** * This activity is used to manage the installed gpu drivers and select one to use. @@ -189,11 +199,28 @@ class GpuDriverActivity : AppCompatActivity() { binding.driverList.addItemDecoration(SpacingItemDecoration(resources.getDimensionPixelSize(R.dimen.grid_padding))) binding.addDriverButton.setOnClickListener { - val intent = Intent(Intent.ACTION_GET_CONTENT).apply { - addFlags(FLAG_GRANT_READ_URI_PERMISSION) - type = "application/zip" - } - installCallback.launch(intent) + val items = arrayOf(getString(R.string.driver_import), getString(R.string.install)) + var checkedItem = 0 + var selectedItem: String? = items[0] + + MaterialAlertDialogBuilder(this) + .setTitle(R.string.choose) + .setSingleChoiceItems(items, checkedItem) { dialog, which -> + selectedItem = items[which] + } + .setPositiveButton(android.R.string.ok) { dialog, _ -> + if (selectedItem == getString(R.string.install)) { + val intent = Intent(Intent.ACTION_GET_CONTENT).apply { + addFlags(FLAG_GRANT_READ_URI_PERMISSION) + type = "application/zip" + } + installCallback.launch(intent) + } else { + handleGpuDriverImport() + } + } + .setNegativeButton(android.R.string.cancel, null) + .show() } populateAdapter() @@ -207,6 +234,72 @@ class GpuDriverActivity : AppCompatActivity() { } } + private fun handleGpuDriverImport() { + val inflater = LayoutInflater.from(this) + val inputBinding = DialogKeyboardBinding.inflate(inflater) + var textInputValue: String = getString(R.string.default_driver_repo_url) + + inputBinding.editTextInput.setText(textInputValue) + inputBinding.editTextInput.doOnTextChanged { text, _, _, _ -> + textInputValue = text.toString() + } + + MaterialAlertDialogBuilder(this) + .setView(inputBinding.root) + .setTitle(R.string.enter_repo_url) + .setPositiveButton(R.string.fetch) { _, _ -> + if (textInputValue.isNotEmpty()) { + fetchAndShowDrivers(textInputValue) + } + } + .setNegativeButton(android.R.string.cancel) {_, _ -> } + .show() + } + + private fun fetchAndShowDrivers(repoUrl: String) { + lifecycleScope.launch(Dispatchers.Main) { + val releases = DriversFetcher.fetchReleases(repoUrl) + if (releases.isEmpty()) { + Snackbar.make(binding.root, "Failed to fetch ${repoUrl}: validation failed or check your internet connection", Snackbar.LENGTH_SHORT).show() + return@launch + } + + val releaseNames = releases.map { it.first } + val releaseUrls = releases.map { it.second } + var chosenUrl: String? = releaseUrls[0] + var chosenName: String? = releaseNames[0] + + MaterialAlertDialogBuilder(this@GpuDriverActivity) + .setTitle(R.string.drivers) + .setSingleChoiceItems(releaseNames.toTypedArray(), 0) { _, which -> + chosenUrl = releaseUrls[which] + chosenName = releaseNames[which] + } + .setPositiveButton(R.string.driver_import) { _, _ -> + downloadDriver(chosenUrl!!, chosenName!!) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + } + + private fun downloadDriver(chosenUrl: String, chosenName: String) { + GlobalScope.launch(Dispatchers.Main) { + var driverFile = File("${SkylineApplication.instance.getPublicFilesDir().canonicalPath}/${chosenName}.zip") + if (!driverFile.exists()) driverFile.createNewFile() + val result = DriversFetcher.downloadAsset(chosenUrl!!, driverFile) + when (result) { + is DownloadResult.Success -> { + val result = GpuDriverHelper.installDriver(this@GpuDriverActivity, FileInputStream(driverFile)) + Snackbar.make(binding.root, resolveInstallResultString(result), Snackbar.LENGTH_LONG).show() + if (result == GpuDriverInstallResult.Success) populateAdapter() + driverFile.delete() + } + is DownloadResult.Error -> Snackbar.make(binding.root, "Failed to import ${chosenName}: ${result.message}", Snackbar.LENGTH_SHORT).show() + } + } + } + private fun resolveInstallResultString(result : GpuDriverInstallResult) = when (result) { GpuDriverInstallResult.Success -> getString(R.string.gpu_driver_install_success) GpuDriverInstallResult.InvalidArchive -> getString(R.string.gpu_driver_install_invalid_archive) diff --git a/app/src/main/java/emu/skyline/utils/DriversFetcher.kt b/app/src/main/java/emu/skyline/utils/DriversFetcher.kt new file mode 100644 index 00000000..dc4b44f4 --- /dev/null +++ b/app/src/main/java/emu/skyline/utils/DriversFetcher.kt @@ -0,0 +1,105 @@ +package emu.skyline.utils + +import android.content.Context +import android.net.Uri +import android.util.Log +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.plugins.logging.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.utils.io.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import java.io.OutputStream +import java.io.FileOutputStream +import java.io.File + +object DriversFetcher { + private val httpClient = HttpClient { + install(ContentNegotiation) { + json(Json { ignoreUnknownKeys = true }) + } + install(Logging) { + level = LogLevel.BODY + } + } + + @Serializable + data class GitHubRelease( + val name: String, + val assets: List = emptyList() + ) + + @Serializable + data class Asset(val browser_download_url: String) + + suspend fun fetchReleases(repoUrl: String): List> { + val repoPath = repoUrl.removePrefix("https://github.com/") + val validationUrl = "https://api.github.com/repos/$repoPath/contents/.adrenoDrivers" + val apiUrl = "https://api.github.com/repos/$repoPath/releases" + + return try { + val isValid = withContext(Dispatchers.IO) { + try { + httpClient.get(validationUrl).status.value == 200 + } catch (e: Exception) { + false + } + } + + if (!isValid) { + Log.d("DriversFetcher", "Provided driver repo url is not valid.") + return emptyList() + } + + val releases: List = withContext(Dispatchers.IO) { + httpClient.get(apiUrl).body() + } + releases.map { release -> + val assetUrl = release.assets.firstOrNull()?.browser_download_url + release.name to assetUrl + } + } catch (e: Exception) { + Log.e("DriversFetcher", "Error fetching releases: ${e.message}", e) + emptyList() + } + } + + suspend fun downloadAsset(assetUrl: String, destinationFile: File): DownloadResult { + return try { + withContext(Dispatchers.IO) { + val response: HttpResponse = httpClient.get(assetUrl) + FileOutputStream(destinationFile)?.use { outputStream -> + writeResponseToStream(response, outputStream) + } ?: return@withContext DownloadResult.Error("Failed to open ${destinationFile.absolutePath.toString()}") + } + DownloadResult.Success + } catch (e: Exception) { + Log.e("DriversFetcher", "Error downloading file: ${e.message}", e) + DownloadResult.Error(e.message) + } + } + + private suspend fun writeResponseToStream(response: HttpResponse, outputStream: OutputStream) { + val channel = response.bodyAsChannel() + val buffer = ByteArray(8192) // 8KB buffer size + + while (!channel.isClosedForRead) { + val bytesRead = channel.readAvailable(buffer) + if (bytesRead > 0) { + outputStream.write(buffer, 0, bytesRead) + } + } + outputStream.flush() + } + + sealed class DownloadResult { + object Success : DownloadResult() + data class Error(val message: String?) : DownloadResult() + } +} diff --git a/app/src/main/res/layout/dialog_keyboard.xml b/app/src/main/res/layout/dialog_keyboard.xml new file mode 100644 index 00000000..a5f75d7c --- /dev/null +++ b/app/src/main/res/layout/dialog_keyboard.xml @@ -0,0 +1,24 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 62759820..af5f1996 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -201,6 +201,12 @@ System Driver The GPU driver provided by your system Install + Import + Choose + Drivers + Enter repo url + https://github.com/K11MCH1/AdrenoToolsDrivers + Fetch Installing the GPU driver… GPU driver installed successfully Failed to unzip the provided driver package diff --git a/build.gradle b/build.gradle index 8a80529f..069de2e3 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { - kotlin_version = '1.9.22' + kotlin_version = '2.0.10' hilt_version = '2.50' }