Skip to content

Commit 142485c

Browse files
committed
Diagnostics: Refuse to deliver a report that carries no settings
A discovery report is only actionable if it names at least one candidate key. Two shapes reached delivery without one, and both cost a maintainer a round-trip that ends in "there is nothing here". An empty matrix was deliverable on purpose: "this ROM keeps the mode elsewhere" is a real result. In practice it is indistinguishable from the two capture mishaps that produce the same output, so the negative result was never trustworthy enough to pay for the reports it let through. REVIEW now has no forward path at all when nothing differed, and the card says so instead of offering "Continue anyway". The second shape is a matrix whose rows were all withheld. A new device's key is never on the auto-disclose allowlist by construction, so the reports worth the most are exactly the ones that arrive empty when the contributor skips the reveal step. Delivery now needs one included row, with a card naming the step rather than leaving Next disabled and unexplained. Withholding individual rows is untouched. Both gates are one predicate, ContributionUiState.deliverable.
1 parent ec07726 commit 142485c

6 files changed

Lines changed: 118 additions & 22 deletions

File tree

app/src/main/java/eu/darken/amply/diagnostics/ui/ContributionWizardScreen.kt

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,12 @@ fun ContributionWizardScreen(
102102
// Not while a capture is in flight — Review must reflect a settled session. Below two modes there
103103
// is nothing to diff, so Review could only ever be empty.
104104
WizardStep.CAPTURE -> state.modes.size >= ContributionWizardViewModel.MIN_MODES && !state.busy
105+
// No settings, no delivery. Both dead ends (nothing differed, or everything withheld) are
106+
// explained by a card in the step itself, so a disabled Next is never unexplained.
107+
WizardStep.REVIEW -> state.deliverable
105108
else -> true
106109
},
107-
// An empty matrix is still deliverable — "the ROM stores this elsewhere" is a real finding — but the
108-
// label has to stop reading like a normal happy-path Next.
109-
nextLabel = if (state.step == WizardStep.REVIEW && state.review.isEmpty()) {
110-
R.string.contribution_next_empty
111-
} else {
112-
R.string.contribution_next
113-
},
110+
nextLabel = R.string.contribution_next,
114111
onBack = onBack,
115112
onNext = onNext,
116113
)
@@ -423,16 +420,19 @@ private fun LazyListScope.reviewStep(
423420
item { EmptyReviewCard(onRestart) }
424421
} else {
425422
item { BodyText(stringResource(R.string.contribution_review_body)) }
423+
if (state.nothingIncluded) {
424+
item { NothingIncludedCard() }
425+
}
426426
items(state.review, key = { it.id.display }) { row ->
427427
ReviewRowCard(row, onRevealRow, onToggleInclude)
428428
}
429429
}
430430
}
431431

432432
/**
433-
* Shown when the captured modes produced no differences at all. Most reports that land here are a capture mishap rather
434-
* than a finding, so the card names the likely causes — but it deliberately does not block delivery: "this ROM keeps the
435-
* mode somewhere else" is exactly the kind of result that should still reach a maintainer.
433+
* Shown when the captured modes produced no differences at all. There is nothing to send from here: the report would
434+
* name no candidate key, and a maintainer cannot tell a capture mishap from a ROM that hides the setting. So the card
435+
* names the likely causes and the only two ways forward — capture again, or accept the device isn't mappable this way.
436436
*/
437437
@Composable
438438
private fun EmptyReviewCard(onRestart: () -> Unit) {
@@ -456,6 +456,25 @@ private fun EmptyReviewCard(onRestart: () -> Unit) {
456456
}
457457
}
458458

459+
/**
460+
* Rows were found but none are included, so the report would carry no candidate key. A new device's key is never on the
461+
* auto-disclose allowlist by definition, so this is the normal state of exactly the reports worth the most — the card
462+
* has to explain the reveal step rather than leave a disabled Next unexplained.
463+
*/
464+
@Composable
465+
private fun NothingIncludedCard() {
466+
AmplyCard(
467+
tone = AmplyCardTone.TertiaryContainer,
468+
verticalArrangement = Arrangement.spacedBy(AmplyCardDefaults.ItemSpacing),
469+
) {
470+
Text(
471+
stringResource(R.string.contribution_review_nothing_included_title),
472+
style = MaterialTheme.typography.titleMedium,
473+
)
474+
Text(stringResource(R.string.contribution_review_nothing_included_body))
475+
}
476+
}
477+
459478
@Composable
460479
private fun ReviewRowCard(
461480
row: ReviewRowUi,

app/src/main/java/eu/darken/amply/diagnostics/ui/ContributionWizardViewModel.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,17 @@ data class ContributionUiState(
7474
* and the ROM version, and those two often identify the ROM family on their own.
7575
*/
7676
val detailsComplete: Boolean get() = featureName.isNotBlank() && romVersion.isNotBlank()
77+
78+
/**
79+
* A report is only worth delivering if it carries at least one setting. This covers both dead ends with one
80+
* predicate: an empty matrix (nothing differed across the captured modes) leaves [review] empty, and a matrix
81+
* whose every row was withheld leaves nothing included. Either way the report would name no candidate key, which
82+
* is the only thing a maintainer can act on.
83+
*/
84+
val deliverable: Boolean get() = review.any { it.included }
85+
86+
/** Rows were found, but none are included yet — distinct from [review] being empty, and fixable by the user. */
87+
val nothingIncluded: Boolean get() = review.isNotEmpty() && !deliverable
7788
}
7889

7990
/**
@@ -218,7 +229,9 @@ class ContributionWizardViewModel @Inject constructor(
218229
WizardStep.CAPTURE -> if (rawSession.observations.size >= MIN_MODES && captureJob?.isActive != true) {
219230
buildReview()
220231
}
221-
WizardStep.REVIEW -> buildDelivery()
232+
// A report with no settings is not deliverable at all: there is no "send it anyway" path, because an
233+
// empty report costs a maintainer a round-trip and tells them nothing the device list doesn't already.
234+
WizardStep.REVIEW -> if (mutableState.value.deliverable) buildDelivery()
222235
WizardStep.DELIVER -> Unit
223236
}
224237
}

app/src/main/res/values/strings.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,9 @@
335335
<string name="contribution_review_empty_title">No differences found</string>
336336
<string name="contribution_review_empty_body">Nothing changed in the secure, global, or system settings between the modes you captured. That usually means one of:</string>
337337
<string name="contribution_review_empty_causes">• The mode wasn\'t actually switched in the system settings between captures.\n• It was switched, but captured before the system applied it. Wait until the manufacturer\'s screen shows the new mode, then capture.\n• This ROM keeps the mode somewhere Amply can\'t read.</string>
338-
<string name="contribution_review_empty_hint">Only the last one is worth sending. If you\'re not sure, start over and capture each mode again.</string>
338+
<string name="contribution_review_empty_hint">There\'s nothing to send from this run, so this is as far as it goes. Start over and capture each mode again if you think one of the first two applies. If your phone has no battery-protection setting to switch, or it keeps it somewhere Amply can\'t read, then this device can\'t be mapped this way.</string>
339+
<string name="contribution_review_nothing_included_title">Nothing included yet</string>
340+
<string name="contribution_review_nothing_included_body">Settings did change, but none are in the report yet. Support for a new device is usually hiding behind one of the rows below, so reveal them, check that nothing personal is in there, and include at least one. A report with no settings can\'t be used.</string>
339341

340342
<!-- Step: deliver -->
341343
<string name="contribution_deliver_title">Send it</string>
@@ -348,7 +350,6 @@
348350

349351
<!-- Navigation -->
350352
<string name="contribution_next">Next</string>
351-
<string name="contribution_next_empty">Continue anyway</string>
352353
<string name="contribution_back">Back</string>
353354

354355
<!-- Dashboard (static UI) -->

app/src/test/java/eu/darken/amply/diagnostics/ui/ContributionWizardEmptyReviewTest.kt

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,22 +81,39 @@ class ContributionWizardEmptyReviewTest {
8181
values = listOf("0", "1"),
8282
)
8383

84+
/** An unknown key: redacted, not revealed, not included — the default state of a new device's real mapping row. */
85+
private val withheldRow = ReviewRowUi(
86+
id = SettingId(SettingNamespace.SECURE, "vendor_unknown_charge_key"),
87+
disclosure = Disclosure.REDACTED,
88+
revealed = false,
89+
included = false,
90+
values = null,
91+
)
92+
8493
@Test
85-
fun `an empty review explains itself instead of reading like a normal step`() {
94+
fun `an empty review explains itself and cannot be delivered`() {
8695
render(reviewState(emptyList()))
8796

8897
compose.onNodeWithText(string(R.string.contribution_review_empty_title)).assertExists()
89-
compose.onNodeWithText(string(R.string.contribution_next_empty)).assertExists()
90-
// The happy-path label must be gone — that is the whole point of the relabel.
91-
compose.onNodeWithText(string(R.string.contribution_next)).assertDoesNotExist()
98+
// No "send it anyway" path: a report with no settings never reaches an issue or the support inbox.
99+
compose.onNodeWithText(string(R.string.contribution_next)).assertIsNotEnabled()
92100
}
93101

94102
@Test
95-
fun `a populated review keeps the normal next label`() {
103+
fun `a review with rows but nothing included cannot be delivered`() {
104+
render(reviewState(listOf(withheldRow)))
105+
106+
compose.onNodeWithText(string(R.string.contribution_review_nothing_included_title)).assertExists()
107+
compose.onNodeWithText(string(R.string.contribution_next)).assertIsNotEnabled()
108+
}
109+
110+
@Test
111+
fun `a populated review can be delivered`() {
96112
render(reviewState(listOf(populatedRow)))
97113

98-
compose.onNodeWithText(string(R.string.contribution_next)).assertExists()
114+
compose.onNodeWithText(string(R.string.contribution_next)).assertIsEnabled()
99115
compose.onNodeWithText(string(R.string.contribution_review_empty_title)).assertDoesNotExist()
116+
compose.onNodeWithText(string(R.string.contribution_review_nothing_included_title)).assertDoesNotExist()
100117
}
101118

102119
// Tall screen: at Robolectric's 470px default the card's action lands behind the bottom bar and the click would

app/src/test/java/eu/darken/amply/diagnostics/ui/ContributionWizardViewModelTest.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,43 @@ class ContributionWizardViewModelTest {
183183
vm.state.value.review.shouldBeEmpty()
184184
}
185185

186+
@Test
187+
fun `an empty matrix cannot be delivered`() = runTest(dispatcher.scheduler) {
188+
val same = mapOf(global("protect_battery") to "0")
189+
val vm = reachedReviewWith(a = same, b = same)
190+
vm.state.value.deliverable shouldBe false
191+
192+
vm.goNext()
193+
advanceUntilIdle()
194+
195+
vm.state.value.step shouldBe WizardStep.REVIEW
196+
vm.state.value.reportText shouldBe null
197+
vm.state.value.issueUrl shouldBe null
198+
}
199+
200+
@Test
201+
fun `a matrix whose rows are all withheld cannot be delivered until one is included`() =
202+
runTest(dispatcher.scheduler) {
203+
// An unknown key is redacted by default, which is exactly what a new device's real mapping row looks like.
204+
val id = secure("vendor_unknown_charge_key")
205+
val vm = reachedReviewWith(a = mapOf(id to "0"), b = mapOf(id to "1"))
206+
vm.state.value.review.single().included shouldBe false
207+
vm.state.value.nothingIncluded shouldBe true
208+
209+
vm.goNext()
210+
advanceUntilIdle()
211+
vm.state.value.step shouldBe WizardStep.REVIEW
212+
213+
vm.revealRow(id)
214+
vm.toggleInclude(id)
215+
vm.state.value.deliverable shouldBe true
216+
217+
vm.goNext()
218+
advanceUntilIdle()
219+
vm.state.value.step shouldBe WizardStep.DELIVER
220+
vm.state.value.reportText.shouldNotBeNull() shouldContain "vendor_unknown_charge_key"
221+
}
222+
186223
@Test
187224
fun `restarting from an empty review clears the captures and returns to capture`() = runTest(dispatcher.scheduler) {
188225
val same = mapOf(global("protect_battery") to "0")

docs/support/kindmail-system-prompt.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,12 @@ that their device will be supported.
131131

132132
### If `changed_rows=0`
133133

134-
Not actionable by itself. The wizard already showed them a "No differences found" warning with the likely
135-
causes and they chose "Continue anyway", so **do not tell them they did it wrong or that they skipped a step.**
136-
They didn't.
134+
**Current builds cannot produce this.** Since the wizard change that followed the first of these emails, a
135+
capture that finds no differences cannot be delivered at all: no issue, no email, only "Start over". A
136+
`changed_rows=0` report arriving now means the user is on 0.2.1-beta0 or older, so ask them to update first.
137+
138+
Not actionable by itself. Older builds warned about it and then offered a "Continue anyway" button, so **do not
139+
tell them they did it wrong or that they skipped a step.** They used a path the app handed them.
137140

138141
There are three causes, and only the last one is a real finding:
139142

@@ -158,6 +161,12 @@ Then check two things in the block and work them into the reply:
158161

159162
- A report with fewer than two captured modes can't be produced by current versions; the wizard blocks it. If
160163
one turns up anyway, the user is on an old build, ask them to update.
164+
- Same for `(no settings approved for inclusion)`, which means settings changed but the contributor included
165+
none of them. Current builds block delivery until at least one row is included, so this shape also only
166+
arrives from 0.2.1-beta0 or older. Don't push the user to disclose anything; just ask them to update and run
167+
it again, and the wizard will explain the reveal step in place.
168+
- Taken together: every discovery report from a current build carries at least one setting. If one doesn't, the
169+
build is old, and that is the first thing to check.
161170
- Never tell a user their device will be supported on the strength of a report. The gate is physical
162171
verification, not a settings mapping.
163172

0 commit comments

Comments
 (0)