From c835b596eaca1f7bc2b7e59bc33f8f614e1626b6 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Fri, 31 Jul 2026 00:16:01 +0200 Subject: [PATCH 1/2] Put the commit first in the build stamp, and compare only that #809 gave the stamp a second half and left the comparison reading the whole string. A canary reports `JingMatrix-Vector-93d66473` and no release SHA starts with that, so `divergesFrom` was true for the very release the reader had just flashed: the install bar warned "same number, other build", the versions sheet coloured that row divergent, and since installed is sameNumber && !diverged, no row was ever marked as installed. The `-dirty` branch it still tested had become unreachable in the same commit. The stamp now leads with the commit -- `93d66473-JingMatrix-Vector` -- so reading it back is a prefix and nothing more, whatever hyphens a repository or a machine turns out to have in its name. A modified tree is marked `+` rather than `-`, in semver's sense of build metadata: after a `-` is a repository holding this exact commit, after a `+` are changes no repository holds. Without that distinction a hand-built framework would claim to be the release it was merely started from. `buildStamp` takes one apart, and `isCommit` compares as a prefix in either direction, since a stamp carries git's short form and a release the full SHA. Where a build was made is deliberately not compared: a fork building the same commit builds the same code. A stamp that names no commit -- "unknown", or the shape published between #809 and here -- is "I cannot tell" rather than divergence, so the canaries already flashed claim neither. On the status page the commit keeps the size it is read at and the rest is set smaller and muted, since it is two thirds of the characters and almost never what the row is looked up for. Copying the page still yields the whole stamp; de-emphasising it must not shorten it. --- build.gradle.kts | 38 +++++++--- manager/README.md | 7 +- .../vector/manager/data/model/BuildStamp.kt | 74 +++++++++++++++++++ .../repository/FrameworkUpdateRepository.kt | 22 ++++-- .../manager/ui/screens/home/HomeViewModel.kt | 7 +- .../ui/screens/home/SystemStatusScreen.kt | 69 +++++++++++++---- .../org/lsposed/lspd/ILSPManagerService.aidl | 9 ++- 7 files changed, 188 insertions(+), 38 deletions(-) create mode 100644 manager/src/main/kotlin/org/matrix/vector/manager/data/model/BuildStamp.kt diff --git a/build.gradle.kts b/build.gradle.kts index b9ef7d796..5e85fae41 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -61,18 +61,26 @@ abstract class GitLatestTagValueSource : ValueSource heading + - items.joinToString("") { "\n ${it.label}: ${it.value}" } + items.joinToString("") { + // With the detail, which the screen only sets apart + // rather than shortens. Where a build came from is + // half of what makes the stamp worth pasting. + "\n ${it.label}: ${it.value}${it.detail.orEmpty()}" + } }, ) scope.launch { snackbars.show(copied, SnackbarTone.Success) } @@ -564,6 +573,12 @@ private fun CrashCard(report: String, onCopy: () -> Unit, onClear: () -> Unit) { * * A fact that can be good or bad says which by its colour, so the page answers "is anything wrong" * before it is read at all. + * + * A row may end in a [InfoItem.detail], set smaller and in the muted colour. It is part of the same + * value and stays in the same line — a reader copying a version out by hand still gets all of it — + * but it is not what the row is looked up for. On the build rows that is where the stamp came from; + * the commit is what someone comparing two devices reads, and the repository or machine after it is + * two thirds of the characters and almost never the answer. */ @Composable private fun InfoRow(row: InfoItem) { @@ -580,7 +595,15 @@ private fun InfoRow(row: InfoItem) { ) Spacer(Modifier.height(2.dp)) Text( - text = row.value, + text = + buildAnnotatedString { + append(row.value) + row.detail?.let { detail -> + val muted = + SpanStyle(fontSize = 12.sp, color = colors.onSurfaceVariant) + withStyle(muted) { append(detail) } + } + }, style = if (row.monospace) VectorMono.copy(fontSize = 15.sp) else MaterialTheme.typography.bodyLarge, @@ -616,11 +639,30 @@ private enum class Health { private data class InfoItem( val label: String, val value: String, + /** The tail of the value that is context rather than identity; set apart, never dropped. */ + val detail: String? = null, val health: Health = Health.Neutral, /** True where the value is an identifier to be compared character by character. */ val monospace: Boolean = true, ) +/** + * A build row: the version number, the commit, and — set apart — where that build was made. + * + * The stamp leads with the commit and says where after it, so the split is the commit's own length + * and needs no second opinion about which half is which. What is left includes the separator, which + * is worth keeping visible: `-` is a repository that holds this exact commit and `+` is a machine + * holding changes that no repository does. + * + * A stamp that names no commit — "unknown", from a build made where git could not be asked — is not + * cut at all. It goes to the muted half whole, because none of it is an identifier. + */ +private fun buildRow(label: String, number: String, reported: String?): InfoItem { + val stamp = reported?.takeIf { it.isNotBlank() } ?: return InfoItem(label, number) + val commit = buildStamp(stamp).commit.orEmpty() + return InfoItem(label, "$number · $commit", detail = stamp.removePrefix(commit)) +} + /** A heading, so the page reads as three short lists rather than one long one. */ @Composable private fun SectionHeading(text: String) { @@ -649,24 +691,23 @@ private fun buildSections( return listOf( str(R.string.info_section_build) to listOf( - InfoItem( + // The exact build, not just its number. Two builds share a version code whenever + // they sit at the same depth on different branches, so the stamp names where the + // build came from as well as the commit: the repository for a CI build, the machine + // for a local one from a modified tree. That is what a bug report needs and what + // the number alone cannot give. + buildRow( str(R.string.info_framework_version), - buildString { - append(status.versionLabel ?: unknown) - // The exact build, not just its number. Two builds share a version code - // whenever they sit at the same depth on different branches, so this names - // where the build came from as well as the commit: the repository for a CI - // build, the machine for a local one from a modified tree. That is what a - // bug report needs and what the number alone cannot give. - status.commit?.takeIf { it.isNotBlank() }?.let { append(" · ").append(it) } - }, + status.versionLabel ?: unknown, + status.commit, ), // Named separately from the framework, because they are flashed separately and are // not always the same build. When these two disagree, that is the answer to a whole // class of "it behaves oddly" reports. - InfoItem( + buildRow( str(R.string.info_manager_version), - "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) · ${BuildConfig.VERSION_HASH}", + "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})", + BuildConfig.VERSION_HASH, ), // Named by which scale the number is on. The two share a field and nothing else: 93 // is a legacy Xposed API, 101 is a libxposed one, and calling both "Xposed API" is diff --git a/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl b/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl index 9b7eb51c7..15ab06767 100644 --- a/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl +++ b/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl @@ -148,10 +148,17 @@ interface ILSPManagerService { void installFrameworkZip(String zipPath, IFrameworkInstallCallback callback) = 57; /** - * The commit this daemon was built from, short, or null when it was not recorded. + * Which build this daemon is, or null when it was not recorded. * * The version code is the commit count on origin/master, so a branch build and the official * build of the same count are indistinguishable by number alone. This is what tells them apart. + * + * Not a bare hash, despite the name: it is the build stamp, which names where the build came + * from as well as what commit it was made from — `93d66473-JingMatrix-Vector` from CI, + * `93d66473` from a clean local tree, `93d66473+thinkpad` from a modified one. The commit + * always leads, so a caller that wants it takes the head and not the whole string; `-` is + * followed by the repository that holds that commit, `+` by the machine holding changes that + * no repository does. */ String getFrameworkCommit() = 58; From e1fd95f0cae91222ddc29657a541b7d6e72419a0 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Fri, 31 Jul 2026 07:10:33 +0200 Subject: [PATCH 2/2] Give the version rows a status column of fixed width In the versions sheet the status sits in whatever room it needs, so the row whose build diverges -- a clause where every other row has one word -- takes that room from the build's name and wraps both the name and its date onto a second line. One row of five is then twice the height of the rest, and the column of names it belongs to is no longer a column. The status now has a width of its own, kept whether or not the row has anything to say, and the name and date are single-line. The clause wraps inside that width instead, which costs nothing: three lines of a label are still shorter than the two lines a name and its date already occupy. --- .../screens/update/FrameworkUpdateScreen.kt | 58 ++++++++++++++----- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateScreen.kt index 01aef5fa6..31bee01e7 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/update/FrameworkUpdateScreen.kt @@ -22,6 +22,7 @@ import androidx.compose.material.icons.rounded.RadioButtonChecked import androidx.compose.material.icons.rounded.History import org.matrix.vector.manager.data.github.ZipVariant import org.matrix.vector.manager.data.github.CanaryArtifact +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SegmentedButtonDefaults @@ -545,6 +546,16 @@ private fun VariantPicker( } } +/** + * How much of a version row the status on its right may take. + * + * Wide enough for "Installed" and "Older" on one line in every language shipped, and narrow enough + * that what is left still holds a build's name and its date without wrapping on a phone. The + * divergence clause is longer than that and wraps inside this width, which is the point of fixing + * it: one row's wordier status must not move where the next row's name begins. + */ +private val STATUS_WIDTH = 96.dp + /** * Every build on this channel, so "no update available" is not a dead end. * @@ -591,7 +602,13 @@ private fun VersionsSheet( onSelect(release) onDismiss() }, - headlineContent = { Text(release.title) }, + // One line each, always. What names the build is the same shape in every + // row — "Vector v2.0 canary 3060", then its date and channel — and a row + // that wraps because of what is beside it reads as a different kind of + // entry when it is not. + headlineContent = { + Text(release.title, maxLines = 1, overflow = TextOverflow.Ellipsis) + }, supportingContent = { Text( listOfNotNull( @@ -603,7 +620,9 @@ private fun VersionsSheet( stringResource(R.string.update_channel_release) }, ) - .joinToString(" · ") + .joinToString(" · "), + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) }, leadingContent = { @@ -625,6 +644,13 @@ private fun VersionsSheet( }, ) }, + // A column of its own width, kept even when this row has nothing to say. + // The status is one word on most rows and a whole clause on the divergent + // one; sized to its content it would take the room the build's name needs + // and wrap that row alone, and a slot that disappears when empty would + // start each row's name in a different place. The label wraps inside its + // column instead, where it costs nothing: three lines of it are still + // shorter than the two the name and its date already occupy. trailingContent = { val label = when { @@ -633,17 +659,23 @@ private fun VersionsSheet( older -> stringResource(R.string.update_older) else -> null } - if (label != null) { - Text( - label, - style = MaterialTheme.typography.labelSmall, - color = - when { - installed -> colors.primary - diverged -> colors.tertiary - else -> colors.onSurfaceVariant - }, - ) + Box( + modifier = Modifier.width(STATUS_WIDTH), + contentAlignment = Alignment.CenterEnd, + ) { + if (label != null) { + Text( + label, + style = MaterialTheme.typography.labelSmall, + textAlign = TextAlign.End, + color = + when { + installed -> colors.primary + diverged -> colors.tertiary + else -> colors.onSurfaceVariant + }, + ) + } } }, colors = sheetRowColors,