Say a build is signed with the wrong key before the installer does

Three things.

A download whose signing certificate does not match the installed
copy is stopped with a dialog naming both digests and offering the
one thing that gets past it: removing the old app. Android's own
answer is "App not installed" with no cause, which reads as the
download having failed. Where either certificate cannot be read the
answer is "don't know" and the install goes ahead as before. The
download is carried on the component's state so that the removal is
followed by the install it was for rather than by a second download.

Pressing that revealed that ACTION_DELETE now needs
REQUEST_DELETE_PACKAGES, and fails invisibly without it -- so the
"Remove the old app" offer for a renamed package had presumably
never worked either.

The build mode and the installed variant are one dropdown, not two.
They answer to the same words, so two pickers offering debug and
release read as one choice asked twice. Where a component declares
modes the mode is the whole answer, and the server serves the build
named after it rather than the newest. Every dropdown now hangs from
one outlined pill with a chevron.

And a checkout parked on a chosen commit is no longer called out of
date, with HEAD in the commit picker as the way back to following
the branch. The commit list comes from that branch rather than from
HEAD, so parking no longer hides the commits after it -- the same
one-way door the tracked-only dirty check closed, in a place that
check did not reach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-02 08:27:03 -04:00
1 parent 3aa6f2f290
commit f983db154d
14 files changed
+895 -111

No files matched your search

+75 -4
View File
@@ -47,8 +47,8 @@ pub enum Freshness {
Current,
/// Its directory has moved past the commit it was built from.
Behind,
/// Never built here, no checkout to compare against, or uncommitted
/// work in its directory.
/// Never built here, no checkout to compare against, uncommitted work
/// in its directory, or a checkout parked on a commit somebody chose.
Unknown,
}
@@ -555,7 +555,25 @@ impl BuildState {
/// notice the new output rather than to produce one. Rebuilding
/// uncommitted work and sending it to a phone would also be a
/// surprising thing to do with work its author has not committed.
pub fn freshness(&self, component: &Component) -> Freshness {
///
/// `parked` says the checkout is sitting on a commit somebody chose
/// rather than following a branch, which is the one way a phone can
/// leave it: picking a commit in the settings sheet detaches HEAD.
/// Saying "out of date" about a checkout somebody deliberately moved
/// backwards is nagging about a decision already made -- and it is
/// nothing to act on either, because every way this reads as behind
/// while parked is a build that failed, a declaration waiting to be
/// accepted, or a component with no build step, each of which is
/// already said on the same card beside the button for it. So while
/// parked a differing commit is reported as unknown: withheld, rather
/// than claimed current, since nothing here measured the output to be
/// what somebody wanted.
///
/// Passed in rather than read here. The caller has already asked git
/// for this project's status, and asking again would be one more
/// process per component on a path fetched on every open, resume and
/// Refresh.
pub fn freshness(&self, component: &Component, parked: bool) -> Freshness {
// Behind for a reason no commit can express: what is on disk was
// built some other way than this component is set to build now.
// Reported before the checkout is consulted at all, because it is
@@ -581,6 +599,7 @@ impl BuildState {
}
match crate::git::subtree_head(&self.project_path, &component.watched_paths()) {
Some(current) if current == built => Freshness::Current,
Some(_) if parked => Freshness::Unknown,
Some(_) => Freshness::Behind,
None => Freshness::Unknown,
}
@@ -1933,10 +1952,62 @@ mod tests {
"a debug build must not satisfy a component set to build release",
);
assert_eq!(
switched.freshness(&switched.components[0]),
switched.freshness(&switched.components[0], false),
Freshness::Behind,
"and the card has to say so rather than reading as current",
);
assert_eq!(
switched.freshness(&switched.components[0], true),
Freshness::Behind,
"a parked checkout says nothing about which mode was built, so this one still \
reports -- what the parking silences is the commit comparison alone",
);
}
/// Picking a commit is a decision, and a card that answers it with
/// "out of date" is nagging about one already made. The comparison
/// itself is unchanged -- what changes is that a *parked* checkout
/// withholds it rather than reporting the difference as being behind.
///
/// Checked against the ordinary case in the same test, because that is
/// the half a change like this can break without looking broken: a
/// checkout following its branch must still say when its build is
/// older than what is checked out.
#[test]
fn a_checkout_parked_on_a_chosen_commit_is_not_reported_as_out_of_date() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
let components = two_component_checkout(root);
let first = crate::git::subtree_head(root, &[PathBuf::from("app")]).expect("a commit");
std::fs::write(root.join("app/main.kt"), "two").expect("write");
run(root, &["git", "commit", "-qam", "two"]);
let state = state_for(root, components);
// Built at the first commit, and the checkout has since moved on:
// the ordinary way to be behind, and the one that must survive.
state
.inner
.lock()
.unwrap()
.built_from
.insert("app".to_string(), first);
let app = state
.components
.iter()
.find(|component| component.name() == "app")
.expect("the app component");
assert_eq!(
state.freshness(app, false),
Freshness::Behind,
"a checkout following its branch still reports a build older than it",
);
assert_eq!(
state.freshness(app, true),
Freshness::Unknown,
"and one parked on a chosen commit withholds it rather than nagging",
);
}
/// The shape of bug a unit test that only calls `component_is_stale`