From 4d00fe1b6011ca4d9b798d3c9a50fc19027f5e79 Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Thu, 10 Sep 2026 00:58:08 -0400 Subject: [PATCH] Skip current APKs during project updates --- AGENTS.md | 9 ++++++ .../com/example/devupdater/UpdaterScreen.kt | 31 ++++++++++++++++--- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7bc57ba..6acb59b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1400,6 +1400,15 @@ mutable at runtime from the phone. in *what* counts as stale, only in building past that answer once any one component tripped it. Regression test: `a_pull_touching_one_component_does_not_rebuild_its_sibling`. +- **A project-wide Update installs only the APKs that are waiting on this + phone.** Skipping a current APK's build was only half of the rule above: + after the pull, `startProjectUpdate` re-read the correctly current + component and then downloaded and offered every APK in the project to the + installer anyway. This showed up on a server-only change as an APK update + despite the card already knowing the installed copy was current. + `needsInstall` is deliberately the one predicate used both to make the + card's Update button pressable and to select the APKs that press installs; + the reason a project needs attention may belong to a sibling component. - **Two words for one measurement is a bug even when both are true.** `MismatchedPairNote` said a sibling still on its old build was "older than this build," while `freshnessNote` already has a word for that diff --git a/app/androidApp/src/main/kotlin/com/example/devupdater/UpdaterScreen.kt b/app/androidApp/src/main/kotlin/com/example/devupdater/UpdaterScreen.kt index 4844a8a..2241b07 100644 --- a/app/androidApp/src/main/kotlin/com/example/devupdater/UpdaterScreen.kt +++ b/app/androidApp/src/main/kotlin/com/example/devupdater/UpdaterScreen.kt @@ -1127,7 +1127,7 @@ private fun AppListScreen( /** * Update is the whole way from the remote to this phone: pull, build what that brought in, and - * install every APK it produced. + * install each APK whose built copy is newer than what is on this phone. * * Written as the two halves in order rather than as a route of its own, because each half is * already a thing this app does and reports: the pull-and-build reports in the project's row @@ -1170,7 +1170,12 @@ private fun AppListScreen( // fired together means the second replaces the first on // screen, and a component silently not installed is worse than // one that says it is waiting. - for (component in fresh.components.filter { it.apk != null }) { + val installed = installedTimes[fresh.key].orEmpty() + val variants = chosenVariants[fresh.key].orEmpty() + for (component in + fresh.components.filter { + needsInstall(it, installed[it.name], variants[it.name]) + }) { // Not the one whose build just failed: its row already // says so, and asking for it again would run the same // failing command a second time to say it twice. @@ -4318,11 +4323,27 @@ private fun hasWorkWaiting( entry.newCommits || entry.components.any { component -> component.isStale || - component.apk?.let { - !isUpToDate(it, installedTimes[component.name], chosenVariants[component.name]) - } == true + needsInstall( + component, + installedTimes[component.name], + chosenVariants[component.name], + ) } +/** + * Whether this component has an APK that would change what is installed on this phone. + * + * Shared by the card's Update button and the project-wide Update loop: once a pull has left an APK + * current, the button's reason for being enabled may belong to a sibling server, and that must not + * make this APK an install candidate anyway. + */ +private fun needsInstall( + component: ProjectComponent, + installedLastUpdateTimeMillis: Long?, + chosenVariantPath: String?, +): Boolean = + component.apk?.let { !isUpToDate(it, installedLastUpdateTimeMillis, chosenVariantPath) } == true + private fun isUpToDate( apk: ComponentApk, installedLastUpdateTimeMillis: Long?,