stats: name the phone's OS instead of only GrapheneOS or other

Reports GrapheneOS, CalyxOS, /e/OS, iodeOS, DivestOS, LineageOS, or stock with the brand.
This commit is contained in:
Seb3thehacker committed 2026-10-11 11:34:06 -07:00
1 parent 33ed2d0653
commit ed331088cc
3 files changed
+33 -6

No files matched your search

+3 -1
View File
@@ -75,8 +75,10 @@
LAUNCHER query below to find, so the setup guide's Android Auto check needs this to see
whether the package is installed at all. -->
<package android:name="com.google.android.projection.gearhead" />
<!-- Every GrapheneOS install has this; the opt-in usage note uses it to say "GrapheneOS". -->
<!-- The opt-in usage note names the OS: GrapheneOS's setup app and /e/OS's app store give
them away (UsageStats.osName). -->
<package android:name="app.grapheneos.setupwizard" />
<package android:name="foundation.e.apps" />
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -205,12 +205,37 @@ object UsageStats {
p.edit().putString(KEY_CARS, cars.toString()).commit()
}
// GrapheneOS reports the same build fields as stock Pixel firmware; its own apps give it away.
private fun isGrapheneOs(context: Context) =
runCatching { context.packageManager.getPackageInfo("app.grapheneos.setupwizard", 0) }.isSuccess
/**
* Which Android this is: a privacy OS by name, or "Stock" with the phone's brand.
*
* GrapheneOS reports the same build fields as stock Pixel firmware, so its own setup app gives
* it away; /e/OS is told apart from the LineageOS it's built on by its app store. The rest set
* build properties of their own. The iodéOS and DivestOS checks are best guesses from their
* build names, not confirmed on a real phone.
*/
fun osName(context: Context): String {
fun installed(pkg: String) = runCatching { context.packageManager.getPackageInfo(pkg, 0) }.isSuccess
val names = listOf("ro.build.flavor", "ro.build.display.id", "ro.lineage.version")
.joinToString(" ") { prop(it) } + " " + Build.DISPLAY + " " + Build.FINGERPRINT
return when {
installed("app.grapheneos.setupwizard") -> "GrapheneOS"
prop("ro.calyxos.version").isNotEmpty() -> "CalyxOS"
installed("foundation.e.apps") -> "/e/OS"
names.contains("iode", ignoreCase = true) -> "iodéOS"
names.contains("divest", ignoreCase = true) -> "DivestOS"
prop("ro.lineage.version").isNotEmpty() -> "LineageOS"
else -> "Stock (${Build.BRAND.replaceFirstChar { it.uppercase() }})"
}
}
/** A system property, or "" when it isn't set or can't be read. */
private fun prop(name: String): String = runCatching {
Class.forName("android.os.SystemProperties").getMethod("get", String::class.java)
.invoke(null, name) as String
}.getOrDefault("")
private fun details(context: Context, c: Choices) = JSONObject().apply {
if (c.android) put("android", Build.VERSION.RELEASE).put("os", if (isGrapheneOs(context)) "GrapheneOS" else "other")
if (c.android) put("android", Build.VERSION.RELEASE).put("os", osName(context))
if (c.phone) put("phone", "${Build.MANUFACTURER} ${Build.MODEL}")
}
@@ -106,7 +106,7 @@ internal fun ColumnScope.UsageStatsRows() {
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 56.dp, end = 16.dp, top = 12.dp, bottom = 4.dp),
)
Choice("Android version, and whether it's GrapheneOS", choices.android) { choices = choices.copy(android = it); save() }
Choice("Android version and system, such as GrapheneOS or stock Samsung", choices.android) { choices = choices.copy(android = it); save() }
Choice("Phone model", choices.phone) { choices = choices.copy(phone = it); save() }
Choice("Each car's head unit, model, year, and screen size, and whether it connected", choices.cars) {
choices = choices.copy(cars = it)