Skip to content

Commit 3af7a0f

Browse files
committed
Merge main into the LineageOS detection fix
#44 landed on the same three files with a related change: the metadata-only report is now gated on ChargingState.hasSupportLead. The two interact. #44's fallback without a lead is "the card keeps the wizard", and this branch withholds the wizard on LineageOS — together those could have left that card with no contribution path at all. They do not: hasSupportLead includes adapterMatched (the Lineage lab adapter matches once detection works) and hasLineageSettingsProvider (verified present on oriole), so the metadata path stays offered while the wizard is withheld. Documented on the parameter, with a preview for that combination since neither existing preview rendered it. Also corrected hasSupportLead itself: it counted `device.lineageOsVersion != null` as a lead, which is dead on every real LineageOS build for exactly the reason this branch exists. Now isLineageOs.
2 parents 4457b46 + bc55ceb commit 3af7a0f

7 files changed

Lines changed: 266 additions & 14 deletions

File tree

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.2.2-beta0 202000
1+
0.2.3-beta0 203000

app/src/main/java/eu/darken/amply/charging/core/ChargingRepository.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,40 @@ data class ChargingState(
9191
writeRequiresShizuku -> access?.shizuku?.ready == true
9292
else -> access?.canControl == true
9393
}
94+
95+
/**
96+
* True when an adapter — live or lab — recognized this device's family, i.e. Amply knows *what* it is looking
97+
* at even when it cannot control it. Derived from [adapterId] rather than carried separately: [AdapterRegistry]
98+
* returns an adapter exactly when some probe matched, and its catch-all is the only null-adapter selection, so a
99+
* mirrored flag could only ever drift.
100+
*/
101+
val adapterMatched: Boolean get() = adapterId != null
102+
103+
/**
104+
* Whether the unprivileged device metadata alone gives a maintainer somewhere to start, which is what makes it
105+
* worth a public device-support issue. Two independent sources, because neither covers the other:
106+
*
107+
* - a matched adapter, including a lab one. Those match on manufacturer or ROM marker, so "Samsung, One UI
108+
* unreadable" still says the feature exists and names the skin whose key mapping to check.
109+
* - a ROM marker or a protection key/provider the probes found directly. [AdapterRegistry]'s family matchers are
110+
* manufacturer lists and property checks, so a rebranded Oplus device or a LineageOS derivative that ships the
111+
* settings provider without the Lineage property lands in the catch-all while still carrying a real lead.
112+
*
113+
* False means every probe came back empty: the report can only state that none of the known families matched,
114+
* which no maintainer can act on. Those devices are pointed at the contribution wizard, which can discover a key
115+
* Amply does not know yet, or at email, where a dead end costs one reply instead of a public issue.
116+
*/
117+
val hasSupportLead: Boolean
118+
get() = adapterMatched ||
119+
device.hasProtectBattery ||
120+
device.hasLineageSettingsProvider ||
121+
device.hasChargingOptimization ||
122+
device.oneUiVersion != null ||
123+
device.hyperOsVersion != null ||
124+
device.oplusRomVersion != null ||
125+
// isLineageOs, not lineageOsVersion: the version property is SELinux-denied to apps and reads back
126+
// empty on every real LineageOS build, so it would contribute nothing here.
127+
device.isLineageOs
94128
}
95129

96130
@Singleton

app/src/main/java/eu/darken/amply/main/ui/dashboard/DashboardScreen.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ fun DashboardScreen(
302302
state.charging.device.manufacturer
303303
.ifBlank { stringResource(R.string.dashboard_manufacturer_fallback) }
304304
},
305+
hasSupportLead = state.charging.hasSupportLead,
305306
reportPreview = state.deviceReport?.let(::formatReport),
306307
showGuidedWizard = state.charging.guidedCaptureUseful,
307308
shizuku = state.charging.access?.shizuku,
@@ -1441,6 +1442,9 @@ private fun DashboardScreenUnsupportedPreview() = PreviewWrapper {
14411442
charging = ChargingState(
14421443
device = DeviceInfo("Samsung", "SM-S911B", 34, "preview", hasChargingOptimization = false),
14431444
adapterName = "Diagnostics only".toCaString(),
1445+
// A Samsung on an unverified One UI: the lab adapter matched, so the metadata-only report
1446+
// names a family to check and the card offers it alongside the wizard.
1447+
adapterId = "samsung-lab",
14441448
controlEnabled = false,
14451449
contributionWanted = true,
14461450
observation = ChargeObservation.Unsupported("This device is not a supported Pixel".toCaString()),

app/src/main/java/eu/darken/amply/main/ui/setup/UnsupportedDeviceCard.kt

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ import eu.darken.amply.common.compose.PreviewWrapper
5252
fun UnsupportedDeviceCard(
5353
modifier: Modifier = Modifier,
5454
platformLabel: String,
55+
/**
56+
* Whether the device metadata alone gives a maintainer somewhere to start (see
57+
* [eu.darken.amply.charging.core.ChargingState.hasSupportLead]). Only then is the metadata-only GitHub
58+
* path offered — it is the one contribution route that needs no Shizuku, but a report naming no family,
59+
* ROM marker or key is a public issue nobody can act on. Without a lead the card offers the wizard,
60+
* which can still discover a key Amply does not know, and email.
61+
*
62+
* On LineageOS both are true at once: a lead exists (the lab adapter matches and the settings provider is
63+
* present), so the metadata path stays offered even though [showGuidedWizard] is false.
64+
*/
65+
hasSupportLead: Boolean,
5566
reportPreview: String?,
5667
showGuidedWizard: Boolean = true,
5768
/** Null while access is still being probed — renders no action rather than guessing at a state. */
@@ -133,15 +144,18 @@ fun UnsupportedDeviceCard(
133144
}
134145
}
135146
}
136-
// Secondary: send just the non-privileged device metadata (no Shizuku needed).
137-
OutlinedButton(
138-
onClick = {
139-
onPrepareReport()
140-
showDialog = true
141-
},
142-
modifier = Modifier.fillMaxWidth(),
143-
) {
144-
Text(stringResource(R.string.setup_unsupported_request_action))
147+
// Secondary: send just the non-privileged device metadata (no Shizuku needed). Offered only where
148+
// that metadata identifies something — see [hasSupportLead].
149+
if (hasSupportLead) {
150+
OutlinedButton(
151+
onClick = {
152+
onPrepareReport()
153+
showDialog = true
154+
},
155+
modifier = Modifier.fillMaxWidth(),
156+
) {
157+
Text(stringResource(R.string.setup_unsupported_request_action))
158+
}
145159
}
146160

147161
HorizontalDivider(Modifier.padding(vertical = 4.dp))
@@ -166,7 +180,9 @@ fun UnsupportedDeviceCard(
166180
}
167181
}
168182

169-
if (showDialog) {
183+
// Also gated, not just its launcher: a lead that disappears under an open dialog must take the
184+
// confirmation with it rather than leaving an Open-GitHub button behind.
185+
if (showDialog && hasSupportLead) {
170186
AlertDialog(
171187
onDismissRequest = { showDialog = false },
172188
title = { Text(stringResource(R.string.setup_unsupported_dialog_title)) },
@@ -221,6 +237,7 @@ private fun UnsupportedDeviceCardPreview() = PreviewWrapper {
221237
UnsupportedDeviceCard(
222238
modifier = Modifier.padding(16.dp),
223239
platformLabel = "Samsung",
240+
hasSupportLead = true,
224241
reportPreview = PREVIEW_REPORT,
225242
onOpenWizard = {},
226243
onPrepareReport = {},
@@ -231,19 +248,39 @@ private fun UnsupportedDeviceCardPreview() = PreviewWrapper {
231248
)
232249
}
233250

251+
/** A device whose metadata carries no lead at all: the metadata-only GitHub path is not offered. */
252+
@AmplyPreview
253+
@Composable
254+
private fun UnsupportedDeviceCardNoLeadPreview() = PreviewWrapper {
255+
UnsupportedDeviceCard(
256+
modifier = Modifier.padding(16.dp),
257+
platformLabel = "BLU",
258+
hasSupportLead = false,
259+
reportPreview = null,
260+
onOpenWizard = {},
261+
onPrepareReport = {},
262+
onCopyReport = {},
263+
onOpenIssue = {},
264+
onEmail = {},
265+
onHelp = {},
266+
)
267+
}
268+
234269
private const val PREVIEW_LINEAGE_REPORT =
235270
"manufacturer=Google\nmodel=Pixel 6\nis_lineageos=true\nlineage_cc_limit_mechanism=NOT_OBSERVED"
236271

237272
/**
238-
* The custom-ROM shape: the guided wizard is withheld (a settings diff can discover nothing there), so the card
239-
* has to carry the Shizuku connect step itself — the state the OEM preview above never renders.
273+
* The custom-ROM shape: a lead exists (lab adapter matched, settings provider present) so the metadata path is
274+
* offered, but the guided wizard is withheld — a settings diff can discover nothing there — which leaves this
275+
* card carrying the Shizuku connect step itself. Neither preview above renders that combination.
240276
*/
241277
@AmplyPreview
242278
@Composable
243279
private fun UnsupportedDeviceCardLineagePreview() = PreviewWrapper {
244280
UnsupportedDeviceCard(
245281
modifier = Modifier.padding(16.dp),
246282
platformLabel = "LineageOS",
283+
hasSupportLead = true,
247284
reportPreview = PREVIEW_LINEAGE_REPORT,
248285
showGuidedWizard = false,
249286
shizuku = BackendStatus(
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package eu.darken.amply.charging.core
2+
3+
import io.kotest.matchers.shouldBe
4+
import org.junit.jupiter.api.Test
5+
6+
/**
7+
* The predicate deciding whether unprivileged device metadata is worth a public device-support issue. It must stay
8+
* wider than "an adapter matched": the family matchers are manufacturer lists and property checks, so a device can
9+
* carry a real lead and still land in the registry's catch-all.
10+
*/
11+
class ChargingStateSupportLeadTest {
12+
13+
private fun device(
14+
manufacturer: String = "BLU",
15+
hasChargingOptimization: Boolean = false,
16+
oneUiVersion: Int? = null,
17+
hyperOsVersion: Int? = null,
18+
oplusRomVersion: Int? = null,
19+
lineageOsVersion: String? = null,
20+
hasProtectBattery: Boolean = false,
21+
hasLineageSettingsProvider: Boolean = false,
22+
) = DeviceInfo(
23+
manufacturer = manufacturer,
24+
model = "B1660V",
25+
sdk = 35,
26+
fingerprint = "test",
27+
hasChargingOptimization = hasChargingOptimization,
28+
oneUiVersion = oneUiVersion,
29+
hyperOsVersion = hyperOsVersion,
30+
oplusRomVersion = oplusRomVersion,
31+
lineageOsVersion = lineageOsVersion,
32+
hasProtectBattery = hasProtectBattery,
33+
hasLineageSettingsProvider = hasLineageSettingsProvider,
34+
)
35+
36+
@Test
37+
fun `a device whose every probe came back empty has no lead`() {
38+
// The BLU B1660V of issue #42: stock Android, no marker, no key, no adapter.
39+
ChargingState(device = device(), adapterId = null).hasSupportLead shouldBe false
40+
}
41+
42+
@Test
43+
fun `a matched adapter is a lead even when no marker was readable`() {
44+
// Samsung whose One UI version won't parse: the lab adapter still names the skin to check.
45+
ChargingState(device = device(manufacturer = "samsung"), adapterId = "samsung-lab")
46+
.hasSupportLead shouldBe true
47+
}
48+
49+
@Test
50+
fun `adapterMatched follows the selected adapter`() {
51+
ChargingState(adapterId = "xiaomi-lab").adapterMatched shouldBe true
52+
ChargingState(adapterId = null).adapterMatched shouldBe false
53+
}
54+
55+
@Test
56+
fun `a ROM marker is a lead even when no adapter matched`() {
57+
// Rebranded Oplus-family hardware: the ROM property is read globally, the matcher is a
58+
// manufacturer list, so this combination reaches the registry's catch-all.
59+
ChargingState(device = device(oplusRomVersion = 16), adapterId = null).hasSupportLead shouldBe true
60+
ChargingState(device = device(oneUiVersion = 9), adapterId = null).hasSupportLead shouldBe true
61+
ChargingState(device = device(hyperOsVersion = 3), adapterId = null).hasSupportLead shouldBe true
62+
ChargingState(device = device(lineageOsVersion = "23.0"), adapterId = null).hasSupportLead shouldBe true
63+
}
64+
65+
@Test
66+
fun `a present protection key or provider is a lead even when no adapter matched`() {
67+
// A LineageOS derivative that ships the settings provider without the Lineage build property.
68+
ChargingState(device = device(hasLineageSettingsProvider = true), adapterId = null)
69+
.hasSupportLead shouldBe true
70+
ChargingState(device = device(hasProtectBattery = true), adapterId = null).hasSupportLead shouldBe true
71+
ChargingState(device = device(hasChargingOptimization = true), adapterId = null).hasSupportLead shouldBe true
72+
}
73+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package eu.darken.amply.main.ui.setup
2+
3+
import android.app.Application
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.rememberScrollState
6+
import androidx.compose.foundation.verticalScroll
7+
import androidx.compose.ui.Modifier
8+
import androidx.compose.ui.test.assertIsEnabled
9+
import androidx.compose.ui.test.assertIsNotEnabled
10+
import androidx.compose.ui.test.junit4.createComposeRule
11+
import androidx.compose.ui.test.onNodeWithText
12+
import androidx.compose.ui.test.performClick
13+
import androidx.test.core.app.ApplicationProvider
14+
import eu.darken.amply.R
15+
import io.kotest.matchers.shouldBe
16+
import org.junit.Rule
17+
import org.junit.Test
18+
import org.junit.runner.RunWith
19+
import org.robolectric.RobolectricTestRunner
20+
import org.robolectric.annotation.GraphicsMode
21+
22+
/**
23+
* Guards which contribution paths an unsupported device is offered. The metadata-only GitHub path is worth a public
24+
* issue only where the metadata names something to chase; for a device whose every probe came back empty it would
25+
* name no family, marker or key at all.
26+
*/
27+
@RunWith(RobolectricTestRunner::class)
28+
@GraphicsMode(GraphicsMode.Mode.NATIVE)
29+
class UnsupportedDeviceCardTest {
30+
31+
@get:Rule
32+
val compose = createComposeRule()
33+
34+
private fun string(res: Int): String =
35+
ApplicationProvider.getApplicationContext<Application>().getString(res)
36+
37+
private fun render(
38+
hasSupportLead: Boolean,
39+
reportPreview: String? = "manufacturer=samsung\nadapter=samsung-lab",
40+
onPrepareReport: () -> Unit = {},
41+
) {
42+
compose.setContent {
43+
Column(Modifier.verticalScroll(rememberScrollState())) {
44+
UnsupportedDeviceCard(
45+
platformLabel = "Samsung",
46+
hasSupportLead = hasSupportLead,
47+
reportPreview = reportPreview,
48+
onOpenWizard = {},
49+
onPrepareReport = onPrepareReport,
50+
onCopyReport = {},
51+
onOpenIssue = {},
52+
onEmail = {},
53+
onHelp = {},
54+
)
55+
}
56+
}
57+
}
58+
59+
@Test
60+
fun `a device with a lead is offered the metadata-only report`() {
61+
render(hasSupportLead = true)
62+
63+
compose.onNodeWithText(string(R.string.setup_unsupported_request_action)).assertExists()
64+
}
65+
66+
@Test
67+
fun `a device without a lead is not offered the metadata-only report`() {
68+
render(hasSupportLead = false)
69+
70+
compose.onNodeWithText(string(R.string.setup_unsupported_request_action)).assertDoesNotExist()
71+
}
72+
73+
@Test
74+
fun `the wizard and email paths stay available without a lead`() {
75+
render(hasSupportLead = false)
76+
77+
compose.onNodeWithText(string(R.string.setup_unsupported_wizard_action)).assertExists()
78+
compose.onNodeWithText(string(R.string.setup_unsupported_email_action)).assertExists()
79+
}
80+
81+
@Test
82+
fun `opening the metadata path requests the report and confirms first`() {
83+
var prepared = false
84+
render(hasSupportLead = true, onPrepareReport = { prepared = true })
85+
86+
compose.onNodeWithText(string(R.string.setup_unsupported_request_action)).performClick()
87+
88+
prepared shouldBe true
89+
// Confirmation, not a direct hand-off: nothing reaches GitHub until the user sees the exact report.
90+
compose.onNodeWithText(string(R.string.setup_unsupported_dialog_title)).assertExists()
91+
compose.onNodeWithText(string(R.string.setup_unsupported_dialog_open)).assertIsEnabled()
92+
}
93+
94+
@Test
95+
fun `the confirmation cannot be accepted until the report snapshot has arrived`() {
96+
// Collection is async: until it lands, what GitHub would receive isn't the previewed text yet.
97+
render(hasSupportLead = true, reportPreview = null)
98+
99+
compose.onNodeWithText(string(R.string.setup_unsupported_request_action)).performClick()
100+
101+
compose.onNodeWithText(string(R.string.setup_unsupported_dialog_open)).assertIsNotEnabled()
102+
compose.onNodeWithText(string(R.string.setup_unsupported_dialog_copy)).assertIsNotEnabled()
103+
}
104+
}

version.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
### Updated by tools/release/bump.sh ###
22
project.versioning.major=0
33
project.versioning.minor=2
4-
project.versioning.patch=2
4+
project.versioning.patch=3
55
project.versioning.build=0
66
project.versioning.type=beta
77
#############################

0 commit comments

Comments
 (0)