mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Add Preview update history
This commit is contained in:
1 parent
255c64551e
commit
2d8befe1cb
7 files changed
+363
-9
No files matched your search
@@ -116,12 +116,12 @@ internal fun UpdatesPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie
|
||||
StatusPanel(s, u, ua, me)
|
||||
Box(Modifier.height(18.dp))
|
||||
ChannelLabel()
|
||||
ChannelPicker(u, ua)
|
||||
ChannelPicker(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(1f)) { ChannelPicker(u, ua, me) }
|
||||
Box(Modifier.weight(1.15f)) { StatusPanel(s, u, ua, me) }
|
||||
}
|
||||
}
|
||||
@@ -260,7 +260,7 @@ private fun ChannelLabel() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelPicker(u: UpdatesState, ua: UpdatesActions) {
|
||||
private fun ChannelPicker(u: UpdatesState, ua: UpdatesActions, me: AppUpdates.Installed) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val catalog = u.catalog
|
||||
val tests = catalog?.tests.orEmpty()
|
||||
@@ -273,6 +273,7 @@ private fun ChannelPicker(u: UpdatesState, ua: UpdatesActions) {
|
||||
Icons.Outlined.Bolt, stringResource(R.string.upd_preview), stringResource(R.string.upd_preview_hint),
|
||||
catalog?.preview?.let { ago(it.publishedAt) }, u.follow.channel == Channel.NIGHTLY,
|
||||
) { ua.onFollow(Follow(Channel.NIGHTLY)) }
|
||||
PreviewHistory(catalog, me)
|
||||
ChannelCard(
|
||||
Icons.Outlined.Science, stringResource(R.string.upd_tests), stringResource(R.string.upd_tests_hint),
|
||||
if (tests.isEmpty()) stringResource(R.string.upd_tests_none) else pluralStringResource(R.plurals.upd_tests_count, tests.size, tests.size),
|
||||
@@ -286,6 +287,63 @@ private fun ChannelPicker(u: UpdatesState, ua: UpdatesActions) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Compact, read-only history under Preview; only the latest Preview build remains installable. */
|
||||
@Composable
|
||||
private fun PreviewHistory(catalog: AppUpdates.Catalog?, me: AppUpdates.Installed) {
|
||||
val history = catalog?.let { AppUpdates.previewHistory(it, me) } ?: return
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val key = catalog?.recentPreviews?.firstOrNull()?.commit
|
||||
var expanded by rememberSaveable(key) { mutableStateOf(false) }
|
||||
val heading = when (history.kind) {
|
||||
AppUpdates.PreviewHistoryKind.CURRENT -> stringResource(R.string.upd_preview_current)
|
||||
AppUpdates.PreviewHistoryKind.BEHIND -> pluralStringResource(
|
||||
R.plurals.upd_preview_behind, history.buildsBehind, history.buildsBehind,
|
||||
)
|
||||
AppUpdates.PreviewHistoryKind.RECENT -> pluralStringResource(
|
||||
R.plurals.upd_preview_recent, history.changes.size, history.changes.size,
|
||||
)
|
||||
}
|
||||
Column(Modifier.fillMaxWidth().padding(start = 66.dp, end = 14.dp, bottom = 5.dp)) {
|
||||
Text(heading, fontSize = 11.5.sp, fontWeight = FontWeight.Medium, color = colors.onSurfaceVariant)
|
||||
if (history.changes.isNotEmpty()) {
|
||||
val visible = if (expanded) history.changes else history.changes.take(3)
|
||||
Column(Modifier.padding(top = 3.dp), verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||
visible.forEach { change ->
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text(
|
||||
changeTitle(change.title.ifBlank { change.commit.take(7) }),
|
||||
fontSize = 12.5.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f),
|
||||
)
|
||||
Text(ago(change.publishedAt), fontSize = 11.sp, color = colors.onSurfaceVariant)
|
||||
}
|
||||
if (change.summary.isNotBlank()) Text(
|
||||
change.summary, fontSize = 11.5.sp, color = colors.onSurfaceVariant,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (history.changes.size > 3 || expanded) {
|
||||
val src = remember { MutableInteractionSource() }
|
||||
val hot = rememberHot(src)
|
||||
val toggle = { expanded = !expanded }
|
||||
Text(
|
||||
if (expanded) stringResource(R.string.upd_less) else stringResource(R.string.upd_more),
|
||||
fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = pal.signal,
|
||||
modifier = Modifier.padding(top = 1.dp).offset(x = (-6).dp).paneItem("preview:history:more")
|
||||
.clip(Shape12).glideBorder(hot, Shape12, pal.signal)
|
||||
.hoverable(src).clickable(interactionSource = src, indication = null, role = Role.Button, onClick = toggle)
|
||||
.controllerConfirm(onClick = toggle)
|
||||
.padding(horizontal = 6.dp, vertical = 4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelCard(
|
||||
icon: ImageVector, title: String, hint: String, latest: String?, selected: Boolean,
|
||||
|
||||
@@ -54,11 +54,19 @@ object AppUpdates {
|
||||
val apk: Apk?,
|
||||
)
|
||||
|
||||
class PreviewChange(
|
||||
val commit: String,
|
||||
val title: String,
|
||||
val summary: String,
|
||||
val publishedAt: Long,
|
||||
)
|
||||
|
||||
class Catalog(
|
||||
val stable: Release?,
|
||||
val preview: Release?,
|
||||
val tests: List<Release>,
|
||||
val checkedAt: Long,
|
||||
val recentPreviews: List<PreviewChange> = emptyList(),
|
||||
)
|
||||
|
||||
class Installed(
|
||||
@@ -68,6 +76,15 @@ object AppUpdates {
|
||||
val versionCode: Int,
|
||||
val updatable: Boolean,
|
||||
val committedAt: Long = 0L,
|
||||
val ciBuild: Boolean = BuildConfig.CI_BUILD,
|
||||
)
|
||||
|
||||
enum class PreviewHistoryKind { CURRENT, BEHIND, RECENT }
|
||||
|
||||
class PreviewHistory(
|
||||
val kind: PreviewHistoryKind,
|
||||
val buildsBehind: Int,
|
||||
val changes: List<PreviewChange>,
|
||||
)
|
||||
|
||||
enum class Offer {
|
||||
@@ -146,6 +163,37 @@ object AppUpdates {
|
||||
r.commit.startsWith(me.commit, ignoreCase = true)
|
||||
}
|
||||
|
||||
/** Recent Preview changes relative to a CI-installed commit, when its position is known. */
|
||||
fun previewHistory(catalog: Catalog, me: Installed = installed()): PreviewHistory? {
|
||||
if (!me.ciBuild || catalog.recentPreviews.isEmpty()) return null
|
||||
val stable = catalog.stable
|
||||
if (
|
||||
me.pr != 0 || stable?.let { isRunning(it, me) } == true ||
|
||||
stable?.version?.let { compareVersions(me.version, it) <= 0 } == true
|
||||
) {
|
||||
return PreviewHistory(PreviewHistoryKind.RECENT, 0, catalog.recentPreviews)
|
||||
}
|
||||
|
||||
val matching = catalog.recentPreviews.indices.filter {
|
||||
matchesInstalledCommit(catalog.recentPreviews[it].commit, me.commit)
|
||||
}
|
||||
if (matching.size != 1) {
|
||||
return PreviewHistory(PreviewHistoryKind.RECENT, 0, catalog.recentPreviews)
|
||||
}
|
||||
val index = matching.single()
|
||||
return if (index == 0) {
|
||||
PreviewHistory(PreviewHistoryKind.CURRENT, 0, emptyList())
|
||||
} else {
|
||||
PreviewHistory(PreviewHistoryKind.BEHIND, index, catalog.recentPreviews.take(index))
|
||||
}
|
||||
}
|
||||
|
||||
private fun matchesInstalledCommit(published: String, installed: String): Boolean {
|
||||
if (!Regex("^[0-9a-fA-F]{7,40}$").matches(installed)) return false
|
||||
return published.equals(installed, ignoreCase = true) ||
|
||||
(installed.length < 40 && published.startsWith(installed, ignoreCase = true))
|
||||
}
|
||||
|
||||
/** Why Android cannot install this published build over [me], before downloading anything. */
|
||||
fun installBlock(r: Release, me: Installed = installed()): String? = when {
|
||||
r.apk == null -> "This build has no download for this copy of DroidDeck."
|
||||
@@ -218,6 +266,7 @@ object AppUpdates {
|
||||
)
|
||||
}
|
||||
val tests = root.optJSONArray("tests") ?: JSONArray()
|
||||
val recentPreviews = root.optJSONArray("recentPreviews") ?: JSONArray()
|
||||
return Catalog(
|
||||
root.optJSONObject("stable")?.let { readPublishedRelease(it, packageName) },
|
||||
root.optJSONObject("preview")?.let { readPublishedRelease(it, packageName) },
|
||||
@@ -226,9 +275,20 @@ object AppUpdates {
|
||||
.filter { it.pr > 0 }
|
||||
.sortedByDescending { it.publishedAt },
|
||||
checkedAt,
|
||||
(0 until recentPreviews.length())
|
||||
.mapNotNull { recentPreviews.optJSONObject(it)?.let(::readPublishedPreviewChange) }
|
||||
.sortedByDescending { it.publishedAt }
|
||||
.distinctBy { it.commit.lowercase() }
|
||||
.take(10),
|
||||
)
|
||||
}
|
||||
|
||||
private fun readPublishedPreviewChange(o: JSONObject): PreviewChange? {
|
||||
val commit = o.optString("commit")
|
||||
if (!Regex("^[0-9a-fA-F]{40}$").matches(commit)) return null
|
||||
return PreviewChange(commit.lowercase(), o.optString("title"), o.optString("summary"), o.optLong("publishedAt"))
|
||||
}
|
||||
|
||||
private fun readPublishedRelease(o: JSONObject, packageName: String): Release {
|
||||
val commit = o.getString("commit")
|
||||
if (!Regex("^[0-9a-fA-F]{7,40}$").matches(commit)) {
|
||||
@@ -358,23 +418,42 @@ object AppUpdates {
|
||||
},
|
||||
)
|
||||
|
||||
private fun writeCatalog(c: Catalog): JSONObject = JSONObject()
|
||||
internal fun writeCatalog(c: Catalog): JSONObject = JSONObject()
|
||||
.put("stable", c.stable?.let(::writeRelease))
|
||||
.put("preview", c.preview?.let(::writeRelease))
|
||||
.put("tests", JSONArray().apply { c.tests.forEach { put(writeRelease(it)) } })
|
||||
.put("recentPreviews", JSONArray().apply { c.recentPreviews.forEach { put(writePreviewChange(it)) } })
|
||||
.put("checkedAt", c.checkedAt)
|
||||
|
||||
private fun readCachedCatalog(o: JSONObject): Catalog {
|
||||
private fun writePreviewChange(c: PreviewChange): JSONObject = JSONObject()
|
||||
.put("commit", c.commit)
|
||||
.put("title", c.title)
|
||||
.put("summary", c.summary)
|
||||
.put("publishedAt", c.publishedAt)
|
||||
|
||||
internal fun readCachedCatalog(o: JSONObject): Catalog {
|
||||
val tests = o.optJSONArray("tests") ?: JSONArray()
|
||||
val preview = o.optJSONObject("preview") ?: o.optJSONObject("nightly")
|
||||
val recentPreviews = o.optJSONArray("recentPreviews") ?: JSONArray()
|
||||
return Catalog(
|
||||
o.optJSONObject("stable")?.let(::readCachedRelease),
|
||||
preview?.let(::readCachedRelease),
|
||||
(0 until tests.length()).map { readCachedRelease(tests.getJSONObject(it)) },
|
||||
o.optLong("checkedAt"),
|
||||
(0 until recentPreviews.length())
|
||||
.mapNotNull { recentPreviews.optJSONObject(it)?.let(::readCachedPreviewChange) }
|
||||
.sortedByDescending { it.publishedAt }
|
||||
.distinctBy { it.commit.lowercase() }
|
||||
.take(10),
|
||||
)
|
||||
}
|
||||
|
||||
private fun readCachedPreviewChange(o: JSONObject): PreviewChange? {
|
||||
val commit = o.optString("commit")
|
||||
if (!Regex("^[0-9a-fA-F]{40}$").matches(commit)) return null
|
||||
return PreviewChange(commit.lowercase(), o.optString("title"), o.optString("summary"), o.optLong("publishedAt"))
|
||||
}
|
||||
|
||||
private fun prefs(context: Context) =
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
}
|
||||
@@ -598,6 +598,7 @@
|
||||
<string name="upd_channel">CANAL DE ACTUALIZACIÓN</string>
|
||||
<string name="upd_stable_hint">Versiones probadas, para la mayoría</string>
|
||||
<string name="upd_preview_hint">Las correcciones más recientes, antes que Stable</string>
|
||||
<string name="upd_preview_current">Al día</string>
|
||||
<string name="upd_tests">Versiones de prueba</string>
|
||||
<string name="upd_tests_hint">Prueba una corrección antes de su publicación</string>
|
||||
<string name="upd_tests_none">Ninguna por ahora</string>
|
||||
@@ -610,6 +611,14 @@
|
||||
<item quantity="one">%1$d para probar</item>
|
||||
<item quantity="other">%1$d para probar</item>
|
||||
</plurals>
|
||||
<plurals name="upd_preview_behind">
|
||||
<item quantity="one">Cambios desde tu versión · %1$d versión atrás</item>
|
||||
<item quantity="other">Cambios desde tu versión · %1$d versiones atrás</item>
|
||||
</plurals>
|
||||
<plurals name="upd_preview_recent">
|
||||
<item quantity="one">Cambios recientes · %1$d versión</item>
|
||||
<item quantity="other">Cambios recientes · %1$d versiones</item>
|
||||
</plurals>
|
||||
<string name="gate_title">Un ajuste de Android por cambiar</string>
|
||||
<string name="gate_lede">Android detiene las apps que inician muchos procesos en segundo plano, y Steam inicia decenas. Hasta que esto cambie, los juegos se cierran sin mostrar ningún error.</string>
|
||||
<string name="gate_fix_for_me">Solucionarlo por mí</string>
|
||||
|
||||
@@ -647,6 +647,7 @@
|
||||
<string name="upd_version_ago">%1$s · %2$s</string>
|
||||
<string name="upd_preview">Preview</string>
|
||||
<string name="upd_preview_hint">Newest fixes, before Stable</string>
|
||||
<string name="upd_preview_current">Up to date</string>
|
||||
<string name="upd_tests">Test builds</string>
|
||||
<string name="upd_tests_hint">Try a fix before it\'s released</string>
|
||||
<string name="upd_tests_none">None right now</string>
|
||||
@@ -660,6 +661,14 @@
|
||||
<item quantity="one">%1$d to try</item>
|
||||
<item quantity="other">%1$d to try</item>
|
||||
</plurals>
|
||||
<plurals name="upd_preview_behind">
|
||||
<item quantity="one">Changes since your build · %1$d build behind</item>
|
||||
<item quantity="other">Changes since your build · %1$d builds behind</item>
|
||||
</plurals>
|
||||
<plurals name="upd_preview_recent">
|
||||
<item quantity="one">Recent changes · %1$d build</item>
|
||||
<item quantity="other">Recent changes · %1$d builds</item>
|
||||
</plurals>
|
||||
<string name="gate_title">One Android setting to change</string>
|
||||
<string name="gate_eyebrow">Steam</string>
|
||||
<string name="gate_lede">Android stops apps that start many background processes, and Steam starts dozens. Until this changes, games close without an error.</string>
|
||||
|
||||
@@ -5,9 +5,12 @@ import com.droiddeck.launcher.update.AppUpdates.Channel
|
||||
import com.droiddeck.launcher.update.AppUpdates.Follow
|
||||
import com.droiddeck.launcher.update.AppUpdates.Installed
|
||||
import com.droiddeck.launcher.update.AppUpdates.Offer
|
||||
import com.droiddeck.launcher.update.AppUpdates.PreviewChange
|
||||
import com.droiddeck.launcher.update.AppUpdates.PreviewHistoryKind
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
@@ -63,7 +66,17 @@ class AppUpdatesTest {
|
||||
pr: Int = 0,
|
||||
version: String = "0.2.0",
|
||||
versionCode: Int = 9,
|
||||
) = Installed(commit, pr, version, versionCode, updatable = true)
|
||||
ciBuild: Boolean = true,
|
||||
) = Installed(commit, pr, version, versionCode, updatable = true, ciBuild = ciBuild)
|
||||
|
||||
private fun change(commit: String, title: String, publishedAt: Long) =
|
||||
PreviewChange(commit, title, "Summary for $title", publishedAt)
|
||||
|
||||
private val recentPreview = listOf(
|
||||
change("1".repeat(40), "Newest", 300L),
|
||||
change("2".repeat(40), "Middle", 200L),
|
||||
change("3".repeat(40), "Oldest", 100L),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun theStableBuildIsCurrentOnStable() {
|
||||
@@ -122,6 +135,76 @@ class AppUpdatesTest {
|
||||
assertNotNull(AppUpdates.installBlock(missing, me("eeeeeee")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun previewHistoryCountsOnlyAnUnambiguousPublishedPreviewCommit() {
|
||||
val withHistory = AppUpdates.Catalog(stable, release("1111111"), emptyList(), 0L, recentPreview)
|
||||
val result = AppUpdates.previewHistory(withHistory, me("2".repeat(40), version = "0.3.0"))!!
|
||||
assertEquals(PreviewHistoryKind.BEHIND, result.kind)
|
||||
assertEquals(1, result.buildsBehind)
|
||||
assertEquals(listOf("Newest"), result.changes.map { it.title })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun currentPreviewHasNoRedundantChangeRows() {
|
||||
val withHistory = AppUpdates.Catalog(stable, release("1111111"), emptyList(), 0L, recentPreview)
|
||||
val result = AppUpdates.previewHistory(withHistory, me("1".repeat(40), version = "0.3.0"))!!
|
||||
assertEquals(PreviewHistoryKind.CURRENT, result.kind)
|
||||
assertTrue(result.changes.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun missingOrAmbiguousPreviewCommitsUseNeutralRecentHistory() {
|
||||
val withHistory = AppUpdates.Catalog(stable, release("1111111"), emptyList(), 0L, recentPreview)
|
||||
assertEquals(
|
||||
PreviewHistoryKind.RECENT,
|
||||
AppUpdates.previewHistory(withHistory, me("9".repeat(40), version = "0.3.0"))!!.kind,
|
||||
)
|
||||
|
||||
val ambiguous = listOf(
|
||||
change("abcdef0" + "1".repeat(33), "First", 300L),
|
||||
change("abcdef0" + "2".repeat(33), "Second", 200L),
|
||||
)
|
||||
val ambiguousCatalog = AppUpdates.Catalog(stable, release("abcdef0"), emptyList(), 0L, ambiguous)
|
||||
val result = AppUpdates.previewHistory(ambiguousCatalog, me("abcdef0", version = "0.3.0"))!!
|
||||
assertEquals(PreviewHistoryKind.RECENT, result.kind)
|
||||
assertEquals(2, result.changes.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun prStableAndLocalBuildsDoNotGetPreviewDistance() {
|
||||
val withHistory = AppUpdates.Catalog(stable, release("1111111"), emptyList(), 0L, recentPreview)
|
||||
assertEquals(
|
||||
PreviewHistoryKind.RECENT,
|
||||
AppUpdates.previewHistory(withHistory, me("2".repeat(40), pr = 93, version = "0.3.0"))!!.kind,
|
||||
)
|
||||
assertEquals(
|
||||
PreviewHistoryKind.RECENT,
|
||||
AppUpdates.previewHistory(withHistory, me(stable.commit))!!.kind,
|
||||
)
|
||||
assertNull(AppUpdates.previewHistory(withHistory, me("2".repeat(40), version = "0.3.0", ciBuild = false)))
|
||||
assertEquals(
|
||||
PreviewHistoryKind.RECENT,
|
||||
AppUpdates.previewHistory(withHistory, me("2".repeat(40), version = "0.1.0"))!!.kind,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cachedPreviewHistorySurvivesRoundTrip() {
|
||||
val withHistory = AppUpdates.Catalog(stable, release("1111111"), emptyList(), 123L, recentPreview)
|
||||
val cached = AppUpdates.readCachedCatalog(AppUpdates.writeCatalog(withHistory))
|
||||
assertEquals(123L, cached.checkedAt)
|
||||
assertEquals(3, cached.recentPreviews.size)
|
||||
assertEquals("Newest", cached.recentPreviews.first().title)
|
||||
assertEquals(recentPreview.first().commit, cached.recentPreviews.first().commit)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun oldCachedCatalogWithoutPreviewHistoryRemainsReadable() {
|
||||
val cached = AppUpdates.readCachedCatalog(JSONObject().put("tests", JSONArray()).put("checkedAt", 123L))
|
||||
assertEquals(123L, cached.checkedAt)
|
||||
assertTrue(cached.recentPreviews.isEmpty())
|
||||
}
|
||||
|
||||
private fun publishedCatalog(signer: String = BuildConfig.RELEASE_SIGNER): JSONObject {
|
||||
val raw = """
|
||||
{
|
||||
@@ -169,6 +252,25 @@ class AppUpdatesTest {
|
||||
assertEquals(9, parsed.preview!!.apk!!.versionCode)
|
||||
assertNull(parsed.preview!!.version)
|
||||
assertEquals(123L, parsed.checkedAt)
|
||||
assertTrue(parsed.recentPreviews.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun publishedCatalogReadsOptionalPreviewHistoryAndDeduplicates() {
|
||||
val first = "a".repeat(40)
|
||||
val newest = "b".repeat(40)
|
||||
val raw = publishedCatalog().put(
|
||||
"recentPreviews",
|
||||
JSONArray()
|
||||
.put(JSONObject().put("commit", first).put("title", "Older").put("summary", "Old").put("publishedAt", 1L))
|
||||
.put(JSONObject().put("commit", newest).put("title", "Newest").put("summary", "New").put("publishedAt", 2L))
|
||||
.put(JSONObject().put("commit", first).put("title", "Duplicate").put("summary", "Again").put("publishedAt", 3L))
|
||||
.put(JSONObject().put("commit", "shortsha").put("title", "Invalid")),
|
||||
)
|
||||
val parsed = AppUpdates.readPublishedCatalog(raw, "com.droiddeck.launcher", 123L)
|
||||
assertEquals(2, parsed.recentPreviews.size)
|
||||
assertEquals("Duplicate", parsed.recentPreviews.first().title)
|
||||
assertEquals(newest, parsed.recentPreviews.last().commit)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -229,6 +229,50 @@ def release_entry(
|
||||
}
|
||||
|
||||
|
||||
def preview_history_entry(release: dict, preview: dict, variants: list[tuple[str, str]]):
|
||||
"""Return compact history metadata for one complete, signed main build."""
|
||||
tag = release.get("tag_name") or ""
|
||||
tag_match = MAIN_TAG.fullmatch(tag)
|
||||
commit = (preview.get("commit") or "").lower()
|
||||
expected_packages = {package for package, _ in variants}
|
||||
apks = preview.get("apks") or {}
|
||||
if (
|
||||
release.get("draft")
|
||||
or not release.get("prerelease")
|
||||
or not tag_match
|
||||
or not re.fullmatch(r"[0-9a-f]{40}", commit)
|
||||
or not commit.startswith(tag_match.group(1))
|
||||
or set(apks) != expected_packages
|
||||
or any(
|
||||
not SHA256.fullmatch((apk.get("sha256") or "").lower())
|
||||
or not SHA256.fullmatch((apk.get("signerSha256") or "").lower())
|
||||
for apk in apks.values()
|
||||
)
|
||||
):
|
||||
return None
|
||||
return {
|
||||
"commit": commit,
|
||||
"title": preview["title"],
|
||||
"summary": preview["summary"][:400],
|
||||
"publishedAt": preview["publishedAt"],
|
||||
}
|
||||
|
||||
|
||||
def recent_previews(entries: list[dict], limit: int = 10) -> list[dict]:
|
||||
"""Keep the newest distinct Preview commits, newest first."""
|
||||
result = []
|
||||
seen = set()
|
||||
for entry in sorted(entries, key=lambda item: item["publishedAt"], reverse=True):
|
||||
commit = entry["commit"].lower()
|
||||
if commit in seen:
|
||||
continue
|
||||
seen.add(commit)
|
||||
result.append({**entry, "commit": commit})
|
||||
if len(result) >= limit:
|
||||
break
|
||||
return result
|
||||
|
||||
|
||||
def release_body_value(body: str, key: str):
|
||||
match = re.search(rf"(?mi)^\s*{re.escape(key)}:\s*`?([^\s`]+)", body or "")
|
||||
return match.group(1) if match else None
|
||||
@@ -276,6 +320,7 @@ def build_catalog(source: GitHub, ci: GitHub, source_repo: str, ci_repo: str) ->
|
||||
|
||||
releases = ci.get(f"/repos/{ci_repo}/releases?per_page=100")
|
||||
previews = []
|
||||
preview_history = []
|
||||
tests = []
|
||||
for release in releases:
|
||||
if release.get("draft"):
|
||||
@@ -291,9 +336,11 @@ def build_catalog(source: GitHub, ci: GitHub, source_repo: str, ci_repo: str) ->
|
||||
except ApiError:
|
||||
continue
|
||||
title, summary = ci_description(release)
|
||||
previews.append(
|
||||
release_entry(release, commit, 0, None, code, variants, signer, title, summary)
|
||||
)
|
||||
preview = release_entry(release, commit, 0, None, code, variants, signer, title, summary)
|
||||
previews.append(preview)
|
||||
history_entry = preview_history_entry(release, preview, variants)
|
||||
if history_entry:
|
||||
preview_history.append(history_entry)
|
||||
continue
|
||||
|
||||
pr_match = PR_TAG.fullmatch(tag)
|
||||
@@ -356,6 +403,7 @@ def build_catalog(source: GitHub, ci: GitHub, source_repo: str, ci_repo: str) ->
|
||||
"generatedAt": int(time.time() * 1000),
|
||||
"stable": stable,
|
||||
"preview": previews[0] if previews else None,
|
||||
"recentPreviews": recent_previews(preview_history),
|
||||
"tests": tests,
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ from tools.release.update_catalog import (
|
||||
parse_gradle,
|
||||
parse_variants,
|
||||
published_millis,
|
||||
preview_history_entry,
|
||||
recent_previews,
|
||||
release_body_value,
|
||||
stable_app_release,
|
||||
)
|
||||
@@ -87,6 +89,53 @@ pubg com.tencent.ig -pubg
|
||||
published_millis(release, prefer_updated=True),
|
||||
)
|
||||
|
||||
def test_preview_history_keeps_only_complete_signed_main_releases(self):
|
||||
variants = [("com.droiddeck.launcher", ""), ("com.example.variant", "-variant")]
|
||||
commit = "a" * 40
|
||||
entry = {
|
||||
"commit": commit,
|
||||
"title": "Fix Steam resume",
|
||||
"summary": "Steam stays open after resuming.",
|
||||
"publishedAt": 123,
|
||||
"apks": {
|
||||
package: {"sha256": "a" * 64, "signerSha256": "b" * 64}
|
||||
for package, _ in variants
|
||||
},
|
||||
}
|
||||
release = {"tag_name": "main-aaaaaaa", "draft": False, "prerelease": True}
|
||||
self.assertEqual(
|
||||
{
|
||||
"commit": commit,
|
||||
"title": "Fix Steam resume",
|
||||
"summary": "Steam stays open after resuming.",
|
||||
"publishedAt": 123,
|
||||
},
|
||||
preview_history_entry(release, entry, variants),
|
||||
)
|
||||
|
||||
for invalid_release in (
|
||||
{**release, "draft": True},
|
||||
{**release, "prerelease": False},
|
||||
{**release, "tag_name": "pr-42"},
|
||||
):
|
||||
self.assertIsNone(preview_history_entry(invalid_release, entry, variants))
|
||||
self.assertIsNone(preview_history_entry(release, {**entry, "commit": "a" * 12}, variants))
|
||||
self.assertIsNone(preview_history_entry(release, {**entry, "apks": {"com.droiddeck.launcher": {}}}, variants))
|
||||
unsigned = {**entry, "apks": {package: {"sha256": "a" * 64} for package, _ in variants}}
|
||||
self.assertIsNone(preview_history_entry(release, unsigned, variants))
|
||||
|
||||
def test_preview_history_is_newest_first_deduplicated_and_limited(self):
|
||||
entries = [
|
||||
{"commit": f"{i:040x}", "title": str(i), "summary": "", "publishedAt": i}
|
||||
for i in range(12)
|
||||
]
|
||||
entries.append({**entries[-1], "title": "duplicate", "publishedAt": 99})
|
||||
recent = recent_previews(entries)
|
||||
self.assertEqual(10, len(recent))
|
||||
self.assertEqual("duplicate", recent[0]["title"])
|
||||
self.assertEqual("000000000000000000000000000000000000000a", recent[1]["commit"])
|
||||
self.assertEqual(1, len([item for item in recent if item["commit"].endswith("000b")]))
|
||||
|
||||
def test_stable_is_the_newest_app_release_not_github_latest(self):
|
||||
def release(tag, at, *assets, **flags):
|
||||
return {"tag_name": tag, "published_at": at, "assets": [{"name": a} for a in assets], **flags}
|
||||
|
||||
Reference in new issue
Block a user