Skip to content

Commit 0f0e4e7

Browse files
committed
General: Fix transient Setup Incomplete card flash during root startup check
On cold start of a rooted device, the root service is re-verified, which takes ~1.2s. During that window RootManager.binder emits a transient null that RootSetupModule maps to a settled Result(ourService=false), flipping SetupManager.State.isIncomplete to true while other modules are still loading. DashboardViewModel.setupCardItem short-circuited on raw isIncomplete before the existing 5s loading-grace debounce, so the setup card rendered immediately and then vanished once the root check finished - the appear-then-disappear flash users could not tap. Add State.isIncompleteSettled (!isLoading && isIncomplete) and gate the immediate card display on it, so transient incomplete states during module probing fall through to the debounce instead of flashing the card. A genuinely incomplete setup still surfaces immediately once probing settles, and the healer-in-progress card state stays visible. RootSetupModule is left unchanged: a null binder emission also represents a genuine root-acquisition failure, which must surface as incomplete; the module cannot distinguish that from the transient cold-start null, so the fix belongs at the dashboard gating layer.
1 parent b57c0b1 commit 0f0e4e7

3 files changed

Lines changed: 101 additions & 1 deletion

File tree

app/src/main/java/eu/darken/sdmse/main/ui/dashboard/DashboardViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ class DashboardViewModel @Inject constructor(
470470
onContinue = { navigateTo(SetupRoute()) }
471471
)
472472

473-
if (setupState.isIncomplete) return@flatMapLatest flowOf(item)
473+
if (setupState.isIncompleteSettled) return@flatMapLatest flowOf(item)
474474

475475
if (!setupState.isLoading) return@flatMapLatest flowOf(null)
476476

app/src/main/java/eu/darken/sdmse/setup/SetupManager.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ class SetupManager @Inject constructor(
6262
val isIncomplete: Boolean = moduleStates.filterIsInstance<SetupModule.State.Current>().any { !it.isComplete }
6363
val isLoading: Boolean = moduleStates.any { it is SetupModule.State.Loading }
6464
val isWorking: Boolean = isHealerWorking || isLoading
65+
66+
// Incomplete only once module probing has settled (isLoading=false). While a module is still
67+
// probing, a privileged check (e.g. the root service handshake on rooted devices) can briefly
68+
// report a settled incomplete Result, which previously flashed the dashboard setup card on
69+
// every launch. The healer may still be working here - that's a deliberately visible card
70+
// state, so it is not excluded.
71+
val isIncompleteSettled: Boolean = !isLoading && isIncomplete
6572
}
6673

6774
companion object {
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package eu.darken.sdmse.setup
2+
3+
import io.kotest.matchers.shouldBe
4+
import org.junit.jupiter.api.Test
5+
import testhelpers.BaseTest
6+
import java.time.Instant
7+
8+
class SetupManagerStateTest : BaseTest() {
9+
10+
private fun loading(at: Instant = Instant.EPOCH) = object : SetupModule.State.Loading {
11+
override val startAt: Instant = at
12+
override val type: SetupModule.Type = SetupModule.Type.ROOT
13+
}
14+
15+
private fun current(complete: Boolean) = object : SetupModule.State.Current {
16+
override val isComplete: Boolean = complete
17+
override val type: SetupModule.Type = SetupModule.Type.ROOT
18+
}
19+
20+
private fun state(
21+
moduleStates: List<SetupModule.State>,
22+
isDismissed: Boolean = false,
23+
isHealerWorking: Boolean = false,
24+
) = SetupManager.State(
25+
moduleStates = moduleStates,
26+
isDismissed = isDismissed,
27+
isHealerWorking = isHealerWorking,
28+
)
29+
30+
@Test fun `incomplete module while another is still loading is not settled-incomplete`() {
31+
// Reproduces the dashboard flash: the root module reports a settled incomplete Result during
32+
// the cold-start handshake while other modules are still probing. isIncomplete is true, but
33+
// the card must NOT be shown immediately - so isIncompleteSettled must be false.
34+
val state = state(
35+
moduleStates = listOf(
36+
loading(),
37+
current(complete = false),
38+
current(complete = true),
39+
),
40+
)
41+
42+
state.isIncomplete shouldBe true
43+
state.isLoading shouldBe true
44+
state.isIncompleteSettled shouldBe false
45+
state.isDone shouldBe false
46+
}
47+
48+
@Test fun `incomplete module once all modules have settled is settled-incomplete`() {
49+
val state = state(
50+
moduleStates = listOf(
51+
current(complete = true),
52+
current(complete = false),
53+
current(complete = true),
54+
),
55+
)
56+
57+
state.isIncomplete shouldBe true
58+
state.isLoading shouldBe false
59+
state.isIncompleteSettled shouldBe true
60+
state.isDone shouldBe false
61+
}
62+
63+
@Test fun `all modules complete is done and not settled-incomplete`() {
64+
val state = state(
65+
moduleStates = listOf(
66+
current(complete = true),
67+
current(complete = true),
68+
),
69+
)
70+
71+
state.isIncomplete shouldBe false
72+
state.isLoading shouldBe false
73+
state.isIncompleteSettled shouldBe false
74+
state.isDone shouldBe true
75+
}
76+
77+
@Test fun `healer working keeps an incomplete card visible even though setup has settled`() {
78+
// The healer running is a deliberately visible card state, so isIncompleteSettled stays true
79+
// when a settled module is incomplete and the healer is still working.
80+
val state = state(
81+
moduleStates = listOf(
82+
current(complete = true),
83+
current(complete = false),
84+
),
85+
isHealerWorking = true,
86+
)
87+
88+
state.isWorking shouldBe true
89+
state.isLoading shouldBe false
90+
state.isIncompleteSettled shouldBe true
91+
state.isDone shouldBe false
92+
}
93+
}

0 commit comments

Comments
 (0)