mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 11:31:08 +00:00
feat: automatically update installed Proton builds on app open
This commit is contained in:
1 parent
74a6a88df8
commit
8a948fbe45
3 files changed
+187
-9
No files matched your search
@@ -91,7 +91,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
private val components = ComponentsMenu(this, ui)
|
||||
private val decky = DeckyMenu(this, ui)
|
||||
private val updates = UpdatesMenu(this, ui)
|
||||
private val protons = ProtonMenu(this, ui)
|
||||
private val protons = ProtonMenu(this, ui) { !busy && pkgStage == null && components.compBusy == null }
|
||||
|
||||
private var installed by mutableStateOf<String?>(null)
|
||||
private var flood by mutableStateOf<PendingFlood?>(null)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.droiddeck.launcher
|
||||
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -8,31 +10,47 @@ import androidx.compose.runtime.setValue
|
||||
import com.droiddeck.launcher.session.ProtonExtras
|
||||
import com.droiddeck.launcher.ui.ProtonRow
|
||||
import com.droiddeck.launcher.session.SessionState
|
||||
import com.droiddeck.launcher.frontend.AutoComponents
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntimeInstaller
|
||||
|
||||
internal class ProtonMenu(private val activity: android.app.Activity, private val ui: android.os.Handler) {
|
||||
internal class ProtonMenu(
|
||||
private val activity: android.app.Activity,
|
||||
private val ui: Handler,
|
||||
private val idle: () -> Boolean = { true },
|
||||
) {
|
||||
var protonRows by mutableStateOf<List<ProtonRow>>(emptyList())
|
||||
var protonBusyId by mutableStateOf<String?>(null)
|
||||
var protonStage by mutableStateOf<String?>(null)
|
||||
var protonPercent by mutableIntStateOf(-1)
|
||||
private val checking = java.util.concurrent.atomic.AtomicBoolean(false)
|
||||
private val autoAttempts = HashMap<String, Pair<String, Long>>()
|
||||
@VisibleForTesting internal var installer: (ProtonExtras.Tool, (String, Int) -> Unit) -> String? =
|
||||
{ tool, progress -> ProtonExtras.install(activity, tool, progress) }
|
||||
|
||||
fun installProton(id: String) {
|
||||
fun installProton(id: String, automatic: Boolean = false) {
|
||||
val tool = ProtonExtras.tools.firstOrNull { it.id == id } ?: return
|
||||
if (protonBusyId != null || SessionState.running) return
|
||||
if (protonBusyId != null || ProtonExtras.installInProgress || SessionState.running) return
|
||||
ProtonExtras.unqueue(activity, tool)
|
||||
protonBusyId = id
|
||||
protonStage = activity.getString(R.string.store_starting)
|
||||
protonPercent = -1
|
||||
Thread({
|
||||
val problem = ProtonExtras.install(activity, tool) { label, value ->
|
||||
ui.post { protonStage = label; protonPercent = value }
|
||||
}
|
||||
val problem = runCatching {
|
||||
installer(tool) { label, value ->
|
||||
ui.post { protonStage = label; protonPercent = value }
|
||||
}
|
||||
}.getOrElse { it.message ?: it.javaClass.simpleName }
|
||||
ui.post {
|
||||
protonBusyId = null
|
||||
protonStage = null
|
||||
protonPercent = -1
|
||||
refreshProtons()
|
||||
if (problem != null) android.widget.Toast.makeText(activity, problem, android.widget.Toast.LENGTH_LONG).show()
|
||||
if (problem != null) {
|
||||
if (automatic) Log.w("ProtonMenu", "auto-update ${tool.id}: $problem")
|
||||
else android.widget.Toast.makeText(activity, problem, android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
if (automatic) ensureAutoUpdates()
|
||||
}
|
||||
}, "install-proton-$id").start()
|
||||
}
|
||||
@@ -64,7 +82,23 @@ internal class ProtonMenu(private val activity: android.app.Activity, private va
|
||||
if (!checking.compareAndSet(false, true)) return
|
||||
Thread({
|
||||
val asked = try { ProtonExtras.checkLatest(activity, force) } catch (_: Throwable) { false } finally { checking.set(false) }
|
||||
if (asked) ui.post { refreshProtons() }
|
||||
ui.post {
|
||||
if (asked) refreshProtons()
|
||||
ensureAutoUpdates()
|
||||
}
|
||||
}, "proton-updates").start()
|
||||
}
|
||||
|
||||
internal fun ensureAutoUpdates(now: Long = System.currentTimeMillis()) {
|
||||
if (activity.isFinishing || activity.isDestroyed || !idle() || protonBusyId != null ||
|
||||
ProtonExtras.installInProgress || SessionState.running || LinuxRuntimeInstaller.isBusy() ||
|
||||
!LinuxRuntime.isInstalled(activity) || !AutoComponents.online(activity) || AutoComponents.unfavourable(activity)) return
|
||||
val (tool, update) = ProtonExtras.tools.firstNotNullOfOrNull { tool ->
|
||||
val update = ProtonExtras.update(activity, tool) ?: return@firstNotNullOfOrNull null
|
||||
val previous = autoAttempts[tool.id]
|
||||
if (previous == null || previous.first != update || now - previous.second !in 0 until 15L * 60 * 1000) tool to update else null
|
||||
} ?: return
|
||||
autoAttempts[tool.id] = update to now
|
||||
installProton(tool.id, automatic = true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.droiddeck.launcher
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import com.droiddeck.launcher.frontend.AutoComponents
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import com.droiddeck.launcher.session.ProtonExtras
|
||||
import com.droiddeck.launcher.session.SessionState
|
||||
import org.json.JSONObject
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.Robolectric
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
import java.io.File
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], manifest = Config.NONE)
|
||||
class ProtonMenuAutoTest {
|
||||
private val online = AutoComponents.online
|
||||
private val constrained = AutoComponents.unfavourable
|
||||
private lateinit var activity: Activity
|
||||
private lateinit var menu: ProtonMenu
|
||||
private lateinit var root: File
|
||||
private val attempts = mutableListOf<String>()
|
||||
private var idle = true
|
||||
|
||||
@Before fun setup() {
|
||||
activity = Robolectric.buildActivity(Activity::class.java).setup().get()
|
||||
root = LinuxRuntime.rootDir(activity)
|
||||
for (path in listOf("usr/bin/gamescope", "usr/local/bin/droiddeck-session", "opt/android-host/proot", "opt/android-host/loader")) {
|
||||
File(root, path).apply { parentFile!!.mkdirs(); writeText("") }
|
||||
}
|
||||
AutoComponents.online = { true }
|
||||
AutoComponents.unfavourable = { false }
|
||||
SessionState.running = false
|
||||
val latest = JSONObject()
|
||||
for (tool in ProtonExtras.tools) latest.put(tool.id, JSONObject()
|
||||
.put("tag", "${tool.prefix}-11-12").put("at", System.currentTimeMillis()))
|
||||
File(activity.filesDir, "proton-latest.json").writeText(latest.toString())
|
||||
menu = ProtonMenu(activity, Handler(Looper.getMainLooper())) { idle }
|
||||
menu.installer = { tool, progress ->
|
||||
progress("Installing ${tool.name}", 50)
|
||||
install("${tool.prefix}-11-12")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@After fun reset() {
|
||||
AutoComponents.online = online
|
||||
AutoComponents.unfavourable = constrained
|
||||
SessionState.running = false
|
||||
}
|
||||
|
||||
private fun install(name: String): File = File(root, "root/.local/share/Steam/compatibilitytools.d/$name").apply {
|
||||
mkdirs()
|
||||
File(this, "toolmanifest.vdf").writeText("manifest")
|
||||
}
|
||||
|
||||
private fun waitUntil(done: () -> Boolean) {
|
||||
val deadline = System.nanoTime() + 5_000_000_000L
|
||||
while (System.nanoTime() < deadline) {
|
||||
shadowOf(Looper.getMainLooper()).idle()
|
||||
if (done()) return
|
||||
Thread.sleep(10)
|
||||
}
|
||||
fail("Proton work did not finish")
|
||||
}
|
||||
|
||||
@Test fun aCachedCheckUpdatesOnlyInstalledFamiliesAndKeepsOlderBuildsAndSelections() {
|
||||
val older = install("DroidDeck-Proton-11-11.1")
|
||||
val request = File(root, "root/.local/share/droiddeck-compat/request.json").apply {
|
||||
parentFile!!.mkdirs()
|
||||
writeText("{\"seq\":1,\"dir\":\"DroidDeck-Proton-11-11.1\",\"valve\":false}")
|
||||
}
|
||||
val before = request.readText()
|
||||
assertFalse(ProtonExtras.checkLatest(activity))
|
||||
menu.checkUpdates()
|
||||
waitUntil { menu.protonRows.singleOrNull { it.id == "droiddeck" }?.installed == "DroidDeck-Proton-11-12" && menu.protonBusyId == null }
|
||||
assertTrue(older.isDirectory)
|
||||
assertEquals(before, request.readText())
|
||||
assertTrue(ProtonExtras.installedAll(activity, ProtonExtras.tools.first { it.id == "ge" }).isEmpty())
|
||||
assertTrue(ProtonExtras.installedAll(activity, ProtonExtras.tools.first { it.id == "cachyos" }).isEmpty())
|
||||
}
|
||||
|
||||
@Test fun downloadsWaitForTheSessionNetworkPowerAndOtherInstallWork() {
|
||||
install("DroidDeck-Proton-11-11.1")
|
||||
menu.installer = { tool, _ -> attempts.add(tool.id); "failed" }
|
||||
SessionState.running = true
|
||||
menu.ensureAutoUpdates()
|
||||
SessionState.running = false
|
||||
AutoComponents.online = { false }
|
||||
menu.ensureAutoUpdates()
|
||||
AutoComponents.online = { true }
|
||||
AutoComponents.unfavourable = { true }
|
||||
menu.ensureAutoUpdates()
|
||||
AutoComponents.unfavourable = { false }
|
||||
idle = false
|
||||
menu.ensureAutoUpdates()
|
||||
assertNull(menu.protonBusyId)
|
||||
assertTrue(attempts.isEmpty())
|
||||
idle = true
|
||||
menu.ensureAutoUpdates()
|
||||
waitUntil { menu.protonBusyId == null }
|
||||
assertEquals(listOf("droiddeck"), attempts)
|
||||
}
|
||||
|
||||
@Test fun aFailedUpdateDoesNotLoopOrBlockOtherInstalledFamilies() {
|
||||
val ge = install("GE-Proton11-11")
|
||||
val dd = install("DroidDeck-Proton-11-11.1")
|
||||
menu.installer = { tool, _ ->
|
||||
attempts.add(tool.id)
|
||||
if (tool.id == "ge") throw IllegalStateException("download failed")
|
||||
install("${tool.prefix}-11-12")
|
||||
null
|
||||
}
|
||||
menu.ensureAutoUpdates()
|
||||
waitUntil { menu.protonBusyId == null }
|
||||
menu.ensureAutoUpdates()
|
||||
shadowOf(Looper.getMainLooper()).idle()
|
||||
assertEquals(listOf("ge", "droiddeck"), attempts)
|
||||
assertTrue(ge.isDirectory)
|
||||
assertTrue(dd.isDirectory)
|
||||
assertNull(menu.protonStage)
|
||||
assertEquals(-1, menu.protonPercent)
|
||||
}
|
||||
|
||||
@Test fun anUninstalledRuntimeOrCurrentProtonDoesNotStartADownload() {
|
||||
install("DroidDeck-Proton-11-12")
|
||||
menu.installer = { tool, _ -> attempts.add(tool.id); null }
|
||||
menu.ensureAutoUpdates()
|
||||
assertTrue(attempts.isEmpty())
|
||||
install("GE-Proton11-11")
|
||||
File(root, "usr/bin/gamescope").delete()
|
||||
menu.ensureAutoUpdates()
|
||||
assertTrue(attempts.isEmpty())
|
||||
assertNull(menu.protonBusyId)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user