mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 03:21:08 +00:00
feat: Decky Loader update channel selector (#1264)
This commit is contained in:
1 parent
003064769d
commit
f67398e6bc
6 files changed
+108
-11
No files matched your search
@@ -11,7 +11,9 @@ import java.io.File
|
||||
|
||||
internal class DeckyMenu(private val activity: android.app.Activity, private val ui: android.os.Handler) {
|
||||
var deckyInstalled by mutableStateOf<String?>(null)
|
||||
var deckyReleases by mutableStateOf<List<DeckyManager.Release>>(emptyList())
|
||||
var deckyLatest by mutableStateOf<DeckyManager.Release?>(null)
|
||||
var deckyBranch by mutableIntStateOf(DeckyManager.BRANCH_STABLE)
|
||||
private var deckyChannels = DeckyManager.ReleaseChannels(emptyList(), emptyList())
|
||||
var deckyChecking by mutableStateOf(false)
|
||||
var deckyStage by mutableStateOf<String?>(null)
|
||||
var deckyPercent by mutableIntStateOf(-1)
|
||||
@@ -22,19 +24,33 @@ internal class DeckyMenu(private val activity: android.app.Activity, private val
|
||||
|
||||
fun refreshDecky() {
|
||||
val request = ++deckyReleaseRequest
|
||||
deckyBranch = DeckyManager.branch(activity)
|
||||
deckyChecking = true
|
||||
Thread({
|
||||
val channels = runCatching { DeckyManager.releaseChannels(activity) }
|
||||
.getOrElse { DeckyManager.ReleaseChannels(emptyList(), emptyList()) }
|
||||
ui.post {
|
||||
if (request == deckyReleaseRequest) {
|
||||
deckyReleases = channels.stable.ifEmpty { channels.prerelease }
|
||||
deckyChannels = channels
|
||||
deckyLatest = DeckyManager.latest(channels, deckyBranch)
|
||||
deckyChecking = false
|
||||
}
|
||||
}
|
||||
}, "decky-releases").start()
|
||||
}
|
||||
|
||||
fun switchDeckyBranch(branch: Int) {
|
||||
if (branch == deckyBranch || deckyStage != null || SessionState.running) return
|
||||
val saved = runCatching { DeckyManager.setBranch(activity, branch) }
|
||||
if (saved.isFailure) {
|
||||
android.widget.Toast.makeText(activity, R.string.decky_channel_failed, android.widget.Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
deckyBranch = branch
|
||||
deckyLatest = DeckyManager.latest(deckyChannels, branch)
|
||||
refreshDecky()
|
||||
}
|
||||
|
||||
fun installDecky(release: DeckyManager.Release) {
|
||||
if (deckyStage != null || SessionState.running) return
|
||||
deckyStage = activity.getString(R.string.store_starting); deckyPercent = -1
|
||||
|
||||
@@ -1649,7 +1649,8 @@ class MainActivity : ComponentActivity() {
|
||||
wifiDiscoveryAsked = wifiDiscoveryAsked,
|
||||
wifiDiscoveryBlocked = wifiDiscoveryBlocked,
|
||||
deckyInstalled = if (mode == SessionService.MODE_STEAM) decky.deckyInstalled else null,
|
||||
deckyLatestRelease = if (mode == SessionService.MODE_STEAM) decky.deckyReleases.firstOrNull() else null,
|
||||
deckyLatestRelease = if (mode == SessionService.MODE_STEAM) decky.deckyLatest else null,
|
||||
deckyBranch = decky.deckyBranch,
|
||||
deckyChecking = decky.deckyChecking, deckyStage = decky.deckyStage, deckyPercent = decky.deckyPercent,
|
||||
deckyDownloadingBinary = decky.deckyDownloadingBinary,
|
||||
deckyEnabled = decky.deckySupervisor, deckySessionRunning = SessionState.running,
|
||||
@@ -1732,6 +1733,7 @@ class MainActivity : ComponentActivity() {
|
||||
onBootMovieChanged = { loadBootMovie() },
|
||||
onDeckyInstall = { release -> decky.installDecky(release) },
|
||||
onDeckyCheck = { decky.refreshDecky() },
|
||||
onDeckyBranch = { branch -> decky.switchDeckyBranch(branch) },
|
||||
onDeckyEnabled = { enabled -> DeckyManager.setSupervisorEnabled(this, enabled); decky.deckySupervisor = enabled },
|
||||
onDeckyUninstall = {
|
||||
DeckyManager.uninstall(this, wipeData = false)
|
||||
|
||||
@@ -22,6 +22,7 @@ object DeckyManager {
|
||||
private const val RELEASES = "https://api.github.com/repos/$REPOSITORY/releases?per_page=30"
|
||||
private const val PATH = "root/homebrew/services/PluginLoader"
|
||||
private const val VERSION = "root/homebrew/services/.droiddeck-decky-version"
|
||||
private const val SETTINGS = "root/homebrew/settings/loader.json"
|
||||
// droiddeck-session reads the marker from the root of the client it runs.
|
||||
private val CEF_REMOTE_DEBUG_MARKERS = listOf(
|
||||
"root/.local/share/Steam/.cef-enable-remote-debugging",
|
||||
@@ -32,7 +33,34 @@ object DeckyManager {
|
||||
private set
|
||||
|
||||
data class Release(val tag: String, val prerelease: Boolean, val asset: String, val url: String, val digest: String?, val shaUrl: String?, val size: Long, val machine: Int)
|
||||
data class ReleaseChannels(val stable: List<Release>, val prerelease: List<Release>)
|
||||
data class ReleaseChannels(val stable: List<Release>, val prerelease: List<Release>, val all: List<Release> = stable + prerelease)
|
||||
|
||||
const val BRANCH_STABLE = 0
|
||||
const val BRANCH_PRERELEASE = 1
|
||||
const val BRANCH_TESTING = 2
|
||||
|
||||
private fun settingsFile(context: Context) = File(LinuxRuntime.rootDir(context), SETTINGS)
|
||||
|
||||
fun branch(context: Context): Int {
|
||||
val saved = runCatching { JSONObject(settingsFile(context).readText()).optInt("branch", -1) }.getOrDefault(-1)
|
||||
if (saved >= 0) return saved
|
||||
val version = File(LinuxRuntime.rootDir(context), VERSION).takeIf { it.isFile }?.readText()?.trim().orEmpty()
|
||||
return if (version.startsWith("v") && "-pre" in version) BRANCH_PRERELEASE else BRANCH_STABLE
|
||||
}
|
||||
|
||||
fun setBranch(context: Context, branch: Int) {
|
||||
val file = settingsFile(context)
|
||||
val settings = runCatching { JSONObject(file.readText()) }.getOrDefault(JSONObject())
|
||||
settings.put("branch", branch)
|
||||
file.parentFile?.mkdirs()
|
||||
val staged = File(file.parentFile, "${file.name}.tmp")
|
||||
staged.writeText(settings.toString(4))
|
||||
check(staged.renameTo(file)) { "cannot write ${file.path}" }
|
||||
}
|
||||
|
||||
// Mirrors Decky's updater: Stable takes the newest non-prerelease tag, Prerelease the newest tag of either kind.
|
||||
fun latest(channels: ReleaseChannels, branch: Int): Release? =
|
||||
if (branch == BRANCH_STABLE) channels.stable.firstOrNull() else channels.all.firstOrNull()
|
||||
|
||||
fun installed(context: Context): String? {
|
||||
val loader = loader(context)
|
||||
@@ -74,17 +102,13 @@ object DeckyManager {
|
||||
}
|
||||
}
|
||||
|
||||
fun releases(context: Context, prerelease: Boolean): List<Release> {
|
||||
val channels = releaseChannels(context)
|
||||
return if (prerelease) channels.prerelease else channels.stable
|
||||
}
|
||||
|
||||
fun releaseChannels(context: Context): ReleaseChannels {
|
||||
val body = Downloader.downloadString(RELEASES) ?: return ReleaseChannels(emptyList(), emptyList())
|
||||
return try {
|
||||
val array = JSONArray(body)
|
||||
val stable = mutableListOf<Release>()
|
||||
val prerelease = mutableListOf<Release>()
|
||||
val all = mutableListOf<Release>()
|
||||
for (i in 0 until array.length()) {
|
||||
val release = array.getJSONObject(i)
|
||||
if (release.optBoolean("draft", false)) continue
|
||||
@@ -105,9 +129,11 @@ object DeckyManager {
|
||||
val size = binary.optLong("size", 0)
|
||||
if (size <= 0) continue
|
||||
val row = Release(release.optString("tag_name", "unknown"), isPrerelease, name, url, digest, checksum, size, if (name == "PluginLoader-arm64") 183 else 62)
|
||||
(if (isPrerelease) prerelease else stable).add(row)
|
||||
if (!row.tag.startsWith("v")) continue
|
||||
all.add(row)
|
||||
(if (isPrerelease || "-pre" in row.tag) prerelease else stable).add(row)
|
||||
}
|
||||
ReleaseChannels(stable, prerelease)
|
||||
ReleaseChannels(stable, prerelease, all)
|
||||
} catch (e: Exception) { Log.w(TAG, "release metadata", e); ReleaseChannels(emptyList(), emptyList()) }
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ class ModeSettings(
|
||||
val wifiDiscoveryBlocked: Boolean = false,
|
||||
val deckyInstalled: String? = null,
|
||||
val deckyLatestRelease: DeckyManager.Release? = null,
|
||||
val deckyBranch: Int = DeckyManager.BRANCH_STABLE,
|
||||
val deckyChecking: Boolean = false,
|
||||
val deckyStage: String? = null,
|
||||
val deckyPercent: Int = -1,
|
||||
@@ -145,6 +146,7 @@ class ModeSettingsActions(
|
||||
val onWifiDiscoverySettings: () -> Unit = {},
|
||||
val onDeckyInstall: (DeckyManager.Release) -> Unit = {},
|
||||
val onDeckyCheck: () -> Unit = {},
|
||||
val onDeckyBranch: (Int) -> Unit = {},
|
||||
val onDeckyEnabled: (Boolean) -> Unit = {},
|
||||
val onDeckyUninstall: () -> Unit = {},
|
||||
val onPickDeckyPluginZip: () -> Unit = {},
|
||||
@@ -424,6 +426,19 @@ fun ModeSheetRows(s: ModeSettings, a: ModeSettingsActions, tab: String, extras:
|
||||
if (s.deckyInstalled != null) DeckyUninstall(enabled = s.deckyStage == null && !s.deckySessionRunning, onUninstall = a.onDeckyUninstall)
|
||||
}
|
||||
}
|
||||
ChoiceRow(
|
||||
host, "decky-channel", stringResource(R.string.decky_channel),
|
||||
stringResource(R.string.decky_channel_hint),
|
||||
listOf(
|
||||
DeckyManager.BRANCH_STABLE to stringResource(R.string.decky_channel_stable),
|
||||
DeckyManager.BRANCH_PRERELEASE to stringResource(R.string.decky_channel_prerelease),
|
||||
) + if (s.deckyBranch == DeckyManager.BRANCH_TESTING) listOf(
|
||||
DeckyManager.BRANCH_TESTING to stringResource(R.string.decky_channel_testing),
|
||||
) else emptyList(),
|
||||
s.deckyBranch,
|
||||
enabled = s.deckyStage == null && !s.deckySessionRunning,
|
||||
onPick = a.onDeckyBranch,
|
||||
)
|
||||
if (s.deckyInstalled != null) ToggleRow(
|
||||
host, "decky-enabled", stringResource(R.string.mode_decky),
|
||||
if (s.deckyEnabled) stringResource(R.string.mode_decky_on)
|
||||
|
||||
@@ -308,6 +308,12 @@
|
||||
<string name="mode_decky_current">Up to date · %1$s</string>
|
||||
<string name="mode_decky_loader">Loader</string>
|
||||
<string name="mode_decky_install_latest">Install latest</string>
|
||||
<string name="decky_channel">Update Channel</string>
|
||||
<string name="decky_channel_hint">Which Decky Loader releases to install. Decky\'s own updater uses the same channel.</string>
|
||||
<string name="decky_channel_stable">Stable</string>
|
||||
<string name="decky_channel_prerelease">Prerelease</string>
|
||||
<string name="decky_channel_testing">Testing</string>
|
||||
<string name="decky_channel_failed">Could not save the Decky update channel</string>
|
||||
<string name="mode_decky_on">Starts with Steam. While on, other apps on this device may be able to control Steam.</string>
|
||||
<string name="mode_decky_off">Off. Steam\'s remote debugging port stays closed.</string>
|
||||
<string name="mode_decky_remove_title">Uninstall Decky Loader?</string>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.droiddeck.launcher.runtime
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class DeckyChannelTest {
|
||||
private fun release(tag: String, prerelease: Boolean) =
|
||||
DeckyManager.Release(tag, prerelease, "PluginLoader-arm64", "https://example.invalid/$tag", null, null, 1, 183)
|
||||
|
||||
@Test fun stableSkipsPrereleasesAndPrereleaseTakesTheNewestTag() {
|
||||
val pre = release("v3.4.0-pre1", true)
|
||||
val stable = release("v3.3.0", false)
|
||||
val channels = DeckyManager.ReleaseChannels(listOf(stable), listOf(pre), listOf(pre, stable))
|
||||
|
||||
assertEquals("v3.3.0", DeckyManager.latest(channels, DeckyManager.BRANCH_STABLE)?.tag)
|
||||
assertEquals("v3.4.0-pre1", DeckyManager.latest(channels, DeckyManager.BRANCH_PRERELEASE)?.tag)
|
||||
}
|
||||
|
||||
@Test fun prereleaseFollowsAStableThatShippedAfterTheLastPrerelease() {
|
||||
val stable = release("v3.3.0", false)
|
||||
val pre = release("v3.3.0-pre1", true)
|
||||
val channels = DeckyManager.ReleaseChannels(listOf(stable), listOf(pre), listOf(stable, pre))
|
||||
|
||||
assertEquals("v3.3.0", DeckyManager.latest(channels, DeckyManager.BRANCH_PRERELEASE)?.tag)
|
||||
}
|
||||
|
||||
@Test fun stableHasNothingWhenOnlyPrereleasesExist() {
|
||||
val pre = release("v3.4.0-pre1", true)
|
||||
assertNull(DeckyManager.latest(DeckyManager.ReleaseChannels(emptyList(), listOf(pre), listOf(pre)), DeckyManager.BRANCH_STABLE))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user