mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0325990b0b | ||
|
|
ba3dc3b455 | ||
|
|
bc2d87be9b | ||
|
|
a800902aa5 | ||
|
|
02f0699b9c |
No files matched your search
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user