Compare commits

...
5 changed files with 56 additions and 13 deletions

No files matched your search

+3
View File
@@ -55,6 +55,8 @@ def sourceCommit = providers.environmentVariable('DROIDDECK_SOURCE_COMMIT').getO
?: providers.environmentVariable('GITHUB_SHA').getOrNull() ?: gitValue(['rev-parse', 'HEAD']) ?: ''
def prNumber = (providers.environmentVariable('DROIDDECK_PR_NUMBER').getOrNull() ?: '0').with { it.isInteger() ? it : '0' }
def ciBuild = providers.environmentVariable('GITHUB_ACTIONS').getOrNull() == 'true'
// The release key's certificate: a build signed with it, from CI or a computer, updates in place.
def releaseSigner = rootProject.file('keystore/release-signer.sha256').text.trim()
// When the source was committed, for "built 3 hours ago": fixed per commit, so it costs no rebuilds.
def commitTime = (gitValue(['show', '-s', '--format=%ct', 'HEAD']) ?: '0').with { it.isLong() ? it : '0' }
// tools/release/variants.txt as "package=suffix;...", so the updater picks the apk of the package it runs as.
@@ -83,6 +85,7 @@ android {
buildConfigField "String", "SOURCE_COMMIT", "\"${sourceCommit}\""
buildConfigField "int", "PR_NUMBER", "${prNumber}"
buildConfigField "boolean", "CI_BUILD", "${ciBuild}"
buildConfigField "String", "RELEASE_SIGNER", "\"${releaseSigner}\""
buildConfigField "long", "COMMIT_TIME", "${commitTime}L * 1000L"
buildConfigField "String", "VARIANT_SUFFIXES", "\"${variantSuffixes}\""
@@ -17,6 +17,7 @@ class App : Application() {
// Before anything else touches the session prefs: a new install's first read decides it.
com.droiddeck.launcher.session.SessionPrefs.settleDeckModeDefault(this)
com.droiddeck.launcher.session.SessionPrefs.settleGpuDriverMode(this)
com.droiddeck.launcher.update.AppUpdates.init(this)
GpuClockPin.clearLeftover(this)
BwrapSpawner.start(this)
}
@@ -112,10 +112,15 @@ internal fun UpdatesPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie
if (LocalNarrowPane.current) {
StatusPanel(s, u, ua, me)
Box(Modifier.height(18.dp))
ChannelLabel()
ChannelPicker(u, ua)
} else Row(horizontalArrangement = Arrangement.spacedBy(18.dp)) {
Box(Modifier.weight(1f)) { ChannelPicker(u, ua) }
Box(Modifier.weight(1.15f)) { StatusPanel(s, u, ua, me) }
} else {
// The label sits over both columns, so the status card lines up with the first channel.
ChannelLabel()
Row(horizontalArrangement = Arrangement.spacedBy(18.dp)) {
Box(Modifier.weight(1f)) { ChannelPicker(u, ua) }
Box(Modifier.weight(1.15f)) { StatusPanel(s, u, ua, me) }
}
}
}
}
@@ -140,13 +145,13 @@ private fun StatusPanel(s: FrontEndState, u: UpdatesState, ua: UpdatesActions, m
val pal = LocalPalette.current
val catalog = u.catalog
val release = catalog?.let { AppUpdates.release(it, u.follow) }
val offer = if (me.ci) catalog?.let { AppUpdates.offer(it, u.follow, me) } else null
val offer = if (me.updatable) catalog?.let { AppUpdates.offer(it, u.follow, me) } else null
val name = channelName(u.follow)
val offered = release != null && (offer == Offer.UPDATE || offer == Offer.SWITCH)
class Look(val tint: Color, val status: String, val headline: String, val detail: String?)
val look = when {
!me.ci -> Look(colors.onSurfaceVariant, "Local build", "Built on a computer",
"It's signed differently from the builds here, so Android won't update it in place. Uninstall it to switch.")
!me.updatable -> Look(colors.onSurfaceVariant, "Signed differently", "Can't update in place",
"This copy isn't signed with DroidDeck's release key, so Android won't install the builds here over it. Uninstall it to switch.")
catalog == null -> Look(colors.onSurfaceVariant, if (u.checking) "Checking…" else "Not checked yet", "Updates", null)
offer == Offer.UPDATE -> Look(AttentionAmber, "Update available", newBuild(u.follow, release!!), null)
offer == Offer.SWITCH -> Look(pal.signal, "Ready to switch", newBuild(u.follow, release!!), null)
@@ -235,13 +240,20 @@ private fun changeTitle(t: String): String =
private fun megabytes(bytes: Long) = "${(bytes + 524_288) / 1_048_576} MB"
/** The three channels as cards to pick from; Test builds opens its list of PRs under it. */
@Composable
private fun ChannelLabel() {
Text(
"UPDATE CHANNEL", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 1.5.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 2.dp, top = 2.dp, bottom = 10.dp),
)
}
@Composable
private fun ChannelPicker(u: UpdatesState, ua: UpdatesActions) {
val colors = MaterialTheme.colorScheme
val catalog = u.catalog
val tests = catalog?.tests.orEmpty()
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text("UPDATE CHANNEL", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 1.5.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(start = 2.dp, top = 2.dp))
ChannelCard(
Icons.Outlined.Verified, "Stable", "Tested releases, for most people",
catalog?.stable?.let { "${it.version ?: it.tag} · ${ago(it.publishedAt)}" }, u.follow.channel == Channel.STABLE,
@@ -1,6 +1,8 @@
package com.droiddeck.launcher.update
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import com.droiddeck.launcher.BuildConfig
import com.droiddeck.launcher.core.Hashes
import org.json.JSONArray
@@ -8,6 +10,7 @@ import org.json.JSONObject
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import java.security.MessageDigest
import java.time.Instant
/**
@@ -47,8 +50,11 @@ object AppUpdates {
class Catalog(val stable: Release?, val nightly: Release?, val tests: List<Release>, val checkedAt: Long)
/** What is running: the commit it was built from, its PR (0 for none), whether CI made it, and when it was committed. */
class Installed(val commit: String, val pr: Int, val version: String, val ci: Boolean, val committedAt: Long = 0L)
/**
* What is running: the commit it was built from, its PR (0 for none), whether the builds here
* install over it (it is signed with the release key), and when it was committed.
*/
class Installed(val commit: String, val pr: Int, val version: String, val updatable: Boolean, val committedAt: Long = 0L)
enum class Offer {
/** This is the build the channel has. */
@@ -63,7 +69,28 @@ object AppUpdates {
GONE,
}
fun installed(): Installed = Installed(BuildConfig.SOURCE_COMMIT, BuildConfig.PR_NUMBER, BuildConfig.VERSION_NAME, BuildConfig.CI_BUILD, BuildConfig.COMMIT_TIME)
@Volatile private var releaseSigned = BuildConfig.CI_BUILD
/** From App.onCreate: whether this copy is signed with the release key, which a local build may be too. */
fun init(context: Context) {
releaseSigned = runCatching { signers(context).any { it.equals(BuildConfig.RELEASE_SIGNER, ignoreCase = true) } }
.getOrDefault(BuildConfig.CI_BUILD)
}
/** SHA-256 of each certificate the package is signed with, including the ones a rotated key vouches for. */
private fun signers(context: Context): List<String> {
val pm = context.packageManager
@Suppress("DEPRECATION")
val certs = if (Build.VERSION.SDK_INT >= 28) {
val info = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNING_CERTIFICATES).signingInfo ?: return emptyList()
if (info.hasMultipleSigners()) info.apkContentsSigners else info.signingCertificateHistory
} else {
pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES).signatures
} ?: return emptyList()
return certs.map { cert -> MessageDigest.getInstance("SHA-256").digest(cert.toByteArray()).joinToString("") { "%02x".format(it) } }
}
fun installed(): Installed = Installed(BuildConfig.SOURCE_COMMIT, BuildConfig.PR_NUMBER, BuildConfig.VERSION_NAME, releaseSigned, BuildConfig.COMMIT_TIME)
fun release(catalog: Catalog, follow: Follow): Release? = when (follow.channel) {
Channel.STABLE -> catalog.stable
@@ -87,7 +114,7 @@ object AppUpdates {
/** The dot on the rail: the followed channel has a newer build of what is running. */
fun hasUpdate(catalog: Catalog?, follow: Follow): Boolean =
catalog != null && installed().ci && offer(catalog, follow) == Offer.UPDATE
catalog != null && installed().updatable && offer(catalog, follow) == Offer.UPDATE
fun isRunning(r: Release, me: Installed = installed()): Boolean {
if (me.commit.length < 7 || r.commit.length < 7 || r.pr != me.pr) return false
@@ -105,7 +132,7 @@ object AppUpdates {
me.pr != 0 -> Follow(Channel.TEST, me.pr)
catalog == null -> return Follow(Channel.STABLE)
catalog.stable != null && isRunning(catalog.stable, me) -> Follow(Channel.STABLE)
me.ci -> Follow(Channel.NIGHTLY)
me.updatable -> Follow(Channel.NIGHTLY)
else -> Follow(Channel.STABLE)
}
setFollow(context, inferred)
@@ -20,7 +20,7 @@ class AppUpdatesTest {
checkedAt = 0L,
)
private fun me(commit: String, pr: Int = 0, version: String = "0.2.0") = Installed(commit, pr, version, ci = true)
private fun me(commit: String, pr: Int = 0, version: String = "0.2.0") = Installed(commit, pr, version, updatable = true)
@Test
fun theStableBuildIsCurrentOnStable() {