mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
example data
This commit is contained in:
1 parent
be981b0899
commit
55320c4d98
4 files changed
+181
-24
No files matched your search
@@ -237,7 +237,7 @@ class GearslipRunner(
|
||||
}
|
||||
log.w("<- ByeByeRequest: reason=$reason ($name) in state $state")
|
||||
log.hex(" byebye raw", body)
|
||||
SessionReport.fail(SessionReport.Category.BYEBYE, "ByeByeRequest reason=$reason ($name)", state.name)
|
||||
SessionReport.fail(SessionReport.Category.BYEBYE, "ByeByeRequest reason=$reason ($name)", state.name, "bye $reason")
|
||||
send(MSG_BYEBYE_RESPONSE, ByteArray(0), encrypted = true)
|
||||
log.i("-> ByeByeResponse")
|
||||
}
|
||||
@@ -316,7 +316,7 @@ class GearslipRunner(
|
||||
sendServiceDiscoveryRequest()
|
||||
}
|
||||
-2 -> {
|
||||
SessionReport.fail(SessionReport.Category.CERTIFICATE, "AuthComplete status -2 (certificate error)", state.name)
|
||||
SessionReport.fail(SessionReport.Category.CERTIFICATE, "AuthComplete status -2 (certificate error)", state.name, "auth -2")
|
||||
log.verdict(
|
||||
"NOT VIABLE (STATUS_CERTIFICATE_ERROR)",
|
||||
"The head unit explicitly rejected the phone's self-signed certificate. " +
|
||||
@@ -330,7 +330,7 @@ class GearslipRunner(
|
||||
state = State.DONE
|
||||
}
|
||||
-3 -> {
|
||||
SessionReport.fail(SessionReport.Category.AUTH, "AuthComplete status -3 (authentication failure)", state.name)
|
||||
SessionReport.fail(SessionReport.Category.AUTH, "AuthComplete status -3 (authentication failure)", state.name, "auth -3")
|
||||
log.verdict(
|
||||
"NOT VIABLE (STATUS_AUTHENTICATION_FAILURE)",
|
||||
"Authentication rejected. Broader than a pure certificate error, but the " +
|
||||
@@ -343,7 +343,7 @@ class GearslipRunner(
|
||||
state = State.DONE
|
||||
}
|
||||
else -> {
|
||||
SessionReport.fail(SessionReport.Category.AUTH, "AuthComplete status $status (unexpected)", state.name)
|
||||
SessionReport.fail(SessionReport.Category.AUTH, "AuthComplete status $status (unexpected)", state.name, "auth $status")
|
||||
log.verdict(
|
||||
"INCONCLUSIVE (AuthComplete status=$status)",
|
||||
"Unexpected status. Check MessageStatus.proto in aasdk for the meaning " +
|
||||
@@ -411,7 +411,7 @@ class GearslipRunner(
|
||||
val video = ServiceDiscovery.findVideoService(serviceDiscoveryResponse)
|
||||
if (video == null) {
|
||||
log.w("no video service in the discovery response - cannot start Phase A")
|
||||
SessionReport.fail(SessionReport.Category.VIDEO, "head unit advertised no video service", state.name)
|
||||
SessionReport.fail(SessionReport.Category.VIDEO, "head unit advertised no video service", state.name, "no video service")
|
||||
return
|
||||
}
|
||||
videoChannelId = video.serviceId
|
||||
@@ -1101,10 +1101,10 @@ class GearslipRunner(
|
||||
|
||||
private fun reportDisconnect() {
|
||||
when (state) {
|
||||
State.WAIT_VERSION -> SessionReport.fail(SessionReport.Category.NO_HANDSHAKE, "no VersionRequest arrived", state.name)
|
||||
State.TLS_HANDSHAKE -> SessionReport.fail(SessionReport.Category.TLS, "head unit dropped the link during TLS", state.name)
|
||||
State.WAIT_AUTH -> SessionReport.fail(SessionReport.Category.AUTH, "dropped after TLS, before AuthComplete", state.name)
|
||||
State.WAIT_SDR -> SessionReport.fail(SessionReport.Category.SERVICE_DISCOVERY, "accepted, then dropped before service discovery", state.name)
|
||||
State.WAIT_VERSION -> SessionReport.fail(SessionReport.Category.NO_HANDSHAKE, "no VersionRequest arrived", state.name, "closed")
|
||||
State.TLS_HANDSHAKE -> SessionReport.fail(SessionReport.Category.TLS, "head unit dropped the link during TLS", state.name, "closed")
|
||||
State.WAIT_AUTH -> SessionReport.fail(SessionReport.Category.AUTH, "dropped after TLS, before AuthComplete", state.name, "closed")
|
||||
State.WAIT_SDR -> SessionReport.fail(SessionReport.Category.SERVICE_DISCOVERY, "accepted, then dropped before service discovery", state.name, "closed")
|
||||
State.DONE -> {}
|
||||
}
|
||||
when (state) {
|
||||
|
||||
@@ -29,12 +29,30 @@ object SessionReport {
|
||||
UNKNOWN("unclassified"),
|
||||
}
|
||||
|
||||
/**
|
||||
* How far a session got, in the order a connection is built. The usage note's "Where
|
||||
* connections fail" share sends the furthest one reached, so a failure says where it broke.
|
||||
*/
|
||||
enum class Stage(val key: String) {
|
||||
USB("usb"), // the link opened; the car may have said nothing
|
||||
VERSIONS("versions"), // the car said which protocol it speaks
|
||||
TLS("tls"), // the secure link is up
|
||||
ACCEPTED("accepted"), // the car accepted the certificate
|
||||
CHANNELS("channels"), // the car listed its channels and said who it is
|
||||
VIDEO("video"), // the car's screen is set up for Gearslip
|
||||
STREAMING("streaming"), // video frames are reaching the car
|
||||
}
|
||||
|
||||
private var appVersion = "?"
|
||||
private var appCode = 0L
|
||||
|
||||
@Volatile private var startedAt = 0L
|
||||
@Volatile var category = Category.NONE; private set
|
||||
@Volatile private var failure = ""
|
||||
@Volatile var stage = Stage.USB; private set
|
||||
/** What the car or the phone said when it broke: a TLS alert, an auth status, a ByeBye reason, an errno. */
|
||||
@Volatile private var failCode = ""
|
||||
@Volatile private var failedAfter = 0L
|
||||
@Volatile private var protocolVersion = ""
|
||||
@Volatile private var certSource = ""
|
||||
@Volatile private var certIssuer = ""
|
||||
@@ -62,6 +80,7 @@ object SessionReport {
|
||||
startedAt = System.currentTimeMillis()
|
||||
category = Category.NONE
|
||||
failure = ""; protocolVersion = ""; certSource = ""; certIssuer = ""
|
||||
stage = Stage.USB; failCode = ""; failedAfter = 0
|
||||
tlsProtocol = ""; tlsCipher = ""; authStatus = null
|
||||
headUnitInfo = null
|
||||
carScreen = ""; carDpi = 0
|
||||
@@ -69,34 +88,87 @@ object SessionReport {
|
||||
printed = false
|
||||
}
|
||||
|
||||
fun protocolVersion(major: Int, minor: Int) { protocolVersion = "$major.$minor" }
|
||||
/** Moves the session's furthest stage on; never back. */
|
||||
private fun reached(next: Stage) { if (next > stage) stage = next }
|
||||
|
||||
fun protocolVersion(major: Int, minor: Int) { protocolVersion = "$major.$minor"; reached(Stage.VERSIONS) }
|
||||
fun certificate(source: String, issuer: String) { certSource = source; certIssuer = issuer }
|
||||
fun tls(protocol: String, cipher: String) { tlsProtocol = protocol; tlsCipher = cipher }
|
||||
fun auth(status: Int) { authStatus = status }
|
||||
fun tls(protocol: String, cipher: String) { tlsProtocol = protocol; tlsCipher = cipher; reached(Stage.TLS) }
|
||||
fun auth(status: Int) { authStatus = status; if (status == 0) reached(Stage.ACCEPTED) }
|
||||
fun headUnit(info: String) { headUnit = info }
|
||||
|
||||
/** Who the car said it is, for the opt-in usage note; null until service discovery. */
|
||||
@Volatile var headUnitInfo: ServiceDiscovery.HeadUnitInfo? = null
|
||||
set(value) { field = value; if (value != null) reached(Stage.CHANNELS) }
|
||||
|
||||
/** The car screen's usable size once its margins are cropped, and its density; blank before video starts. */
|
||||
@Volatile var carScreen = ""; private set
|
||||
@Volatile var carDpi = 0; private set
|
||||
fun carScreen(size: String, dpi: Int) { carScreen = size; carDpi = dpi }
|
||||
fun carScreen(size: String, dpi: Int) { carScreen = size; carDpi = dpi; reached(Stage.VIDEO) }
|
||||
fun profile(name: String?) { profile = name ?: "none matched" }
|
||||
fun video(summary: String) { video = summary }
|
||||
fun audio(summary: String) { audio = summary }
|
||||
fun stream(summary: String) { lastStream = summary }
|
||||
fun stream(summary: String) { lastStream = summary; reached(Stage.STREAMING) }
|
||||
|
||||
/** Records the first failure only: later ones are usually fallout from it. */
|
||||
fun fail(category: Category, detail: String, state: String = "") {
|
||||
/**
|
||||
* Records the first failure only: later ones are usually fallout from it. [code] is the short
|
||||
* thing the car or the phone said, like "auth -3" or "bye 1", for the usage note.
|
||||
*/
|
||||
fun fail(category: Category, detail: String, state: String = "", code: String = "") {
|
||||
if (this.category != Category.NONE) return
|
||||
this.category = category
|
||||
failure = detail
|
||||
lastState = state
|
||||
failCode = code
|
||||
failedAfter = if (startedAt == 0L) 0 else (System.currentTimeMillis() - startedAt) / 1000
|
||||
}
|
||||
|
||||
fun fail(category: Category, detail: String, t: Throwable, state: String = "") =
|
||||
fail(category, "$detail (${GearslipLog.rootCause(t)})", state)
|
||||
fail(category, "$detail (${GearslipLog.rootCause(t)})", state, codeOf(t))
|
||||
|
||||
/**
|
||||
* A short code from an exception, never its whole message: the TLS alert's name, the errno
|
||||
* of a USB failure, or else the exception's class.
|
||||
*/
|
||||
private fun codeOf(t: Throwable): String {
|
||||
var root = t
|
||||
while (root.cause != null && root.cause !== root) root = root.cause!!
|
||||
val text = "${t.message} ${root.message}"
|
||||
Regex("(?i)alert[ _:]*\\(?([a-z_ ]{3,40}?)\\)?(?:\\s|$|[,;.])").find(text)?.let {
|
||||
return "alert ${it.groupValues[1].trim().lowercase().replace(' ', '_')}"
|
||||
}
|
||||
Regex("\\b(E[A-Z]{2,12})\\b").find(text)?.let { return it.groupValues[1] }
|
||||
return root.javaClass.simpleName.take(40)
|
||||
}
|
||||
|
||||
/**
|
||||
* One session for the usage note's "Where connections fail" share: how far it got, how and
|
||||
* when it ended, and with which kind of certificate. Counts and codes only, nothing personal.
|
||||
*/
|
||||
fun sessionNote(): org.json.JSONObject {
|
||||
val seconds = if (startedAt == 0L) 0 else (System.currentTimeMillis() - startedAt) / 1000
|
||||
val source = certSource.lowercase()
|
||||
return org.json.JSONObject()
|
||||
.put("stage", stage.key)
|
||||
.put("ended", category.name.lowercase())
|
||||
.put("code", failCode)
|
||||
.put("failed_at", if (category == Category.NONE) 0 else rounded(failedAfter))
|
||||
.put("seconds", rounded(seconds))
|
||||
.put("cert", when {
|
||||
"imported" in source -> "imported"
|
||||
"downloaded" in source -> "downloaded"
|
||||
"self" in source -> "self-signed"
|
||||
source.isEmpty() -> ""
|
||||
else -> "other"
|
||||
})
|
||||
}
|
||||
|
||||
/** Seconds rounded so a drive's length can't be matched to anything: 5 s, then minutes, then 5 minutes. */
|
||||
private fun rounded(s: Long): Long = when {
|
||||
s < 60 -> (s + 2) / 5 * 5
|
||||
s < 3600 -> (s + 30) / 60 * 60
|
||||
else -> (s + 150) / 300 * 300
|
||||
}
|
||||
|
||||
/** The report as plain text, for the log file and for anything that gets sent. */
|
||||
fun render(): String {
|
||||
@@ -145,7 +217,9 @@ object SessionReport {
|
||||
category == Category.NONE -> "ended before video"
|
||||
else -> category.label
|
||||
}
|
||||
runCatching { UsageStats.recordSession(context, headUnitInfo, protocolVersion, outcome, carScreen, carDpi) }
|
||||
runCatching {
|
||||
UsageStats.recordSession(context, headUnitInfo, protocolVersion, outcome, carScreen, carDpi, sessionNote())
|
||||
}
|
||||
runCatching { app.seb3thehacker.gearslip.update.UpdateChecker.onDriveEnded(context) }
|
||||
}
|
||||
GearslipLog.flush()
|
||||
|
||||
@@ -32,6 +32,8 @@ object UsageStats {
|
||||
val android: Boolean = true,
|
||||
val phone: Boolean = true,
|
||||
val cars: Boolean = true,
|
||||
/** How each car session went: how far it got, how and when it ended. Off until ticked. */
|
||||
val failures: Boolean = false,
|
||||
)
|
||||
|
||||
private const val PREFS = "usage_stats"
|
||||
@@ -49,6 +51,10 @@ object UsageStats {
|
||||
/** The daily background job of earlier builds, cancelled wherever it's still scheduled. */
|
||||
private const val OLD_JOB_ID = 4_207
|
||||
private const val MAX_CARS = 10
|
||||
/** Each session for the "Where connections fail" share, waiting for the next note. */
|
||||
private const val KEY_SESSIONS = "sessions"
|
||||
private const val KEY_LAST_SESSIONS = "last_sessions"
|
||||
private const val MAX_SESSIONS = 20
|
||||
|
||||
private fun prefs(context: Context) = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
|
||||
@@ -69,6 +75,7 @@ object UsageStats {
|
||||
android = p.getBoolean("share_android", true),
|
||||
phone = p.getBoolean("share_phone", true),
|
||||
cars = p.getBoolean("share_cars", true),
|
||||
failures = p.getBoolean("share_failures", false),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -79,10 +86,13 @@ object UsageStats {
|
||||
.putBoolean("share_android", choices.android)
|
||||
.putBoolean("share_phone", choices.phone)
|
||||
.putBoolean("share_cars", choices.cars)
|
||||
.putBoolean("share_failures", choices.failures)
|
||||
.apply()
|
||||
// Turning it on sends nothing yet: the driver may still be ticking boxes. The first note
|
||||
// waits for [choicesClosed].
|
||||
if (!enabled) prefs(context).edit().remove(KEY_CARS).remove(KEY_LAST_CARS).remove(KEY_SENT_DAY).apply()
|
||||
if (!enabled) prefs(context).edit().remove(KEY_CARS).remove(KEY_LAST_CARS).remove(KEY_SENT_DAY)
|
||||
.remove(KEY_SESSIONS).remove(KEY_LAST_SESSIONS).apply()
|
||||
if (!choices.failures) prefs(context).edit().remove(KEY_SESSIONS).remove(KEY_LAST_SESSIONS).apply()
|
||||
}
|
||||
|
||||
/** The driver left the screen with the usage note choices: send today's note if it's due. */
|
||||
@@ -90,10 +100,15 @@ object UsageStats {
|
||||
if (prefs(context).getString(KEY_SENT_DAY, null) != LocalDate.now().toString()) sendSoon(context)
|
||||
}
|
||||
|
||||
/** Gearslip was opened: send today's note unless one already went out. */
|
||||
/**
|
||||
* Gearslip was opened: send today's note unless one already went out, or sooner if a session
|
||||
* is still waiting, like one that ended in a crash before its note could go.
|
||||
*/
|
||||
fun onAppOpened(context: Context) {
|
||||
context.getSystemService(JobScheduler::class.java)?.cancel(OLD_JOB_ID)
|
||||
if (prefs(context).getString(KEY_SENT_DAY, null) != LocalDate.now().toString()) sendSoon(context)
|
||||
val p = prefs(context)
|
||||
val waiting = p.getString(KEY_CARS, "[]") != "[]" || p.getString(KEY_SESSIONS, "[]") != "[]"
|
||||
if (waiting || p.getString(KEY_SENT_DAY, null) != LocalDate.now().toString()) sendSoon(context)
|
||||
}
|
||||
|
||||
private val sending = AtomicBoolean(false)
|
||||
@@ -134,13 +149,35 @@ object UsageStats {
|
||||
outcome: String,
|
||||
screen: String = "",
|
||||
dpi: Int = 0,
|
||||
session: JSONObject? = null,
|
||||
) {
|
||||
if (!enabled(context)) return
|
||||
val p = prefs(context)
|
||||
synchronized(carsLock) { addCar(p, info, protocol, outcome, screen, dpi) }
|
||||
synchronized(carsLock) {
|
||||
addCar(p, info, protocol, outcome, screen, dpi)
|
||||
if (session != null && choices(context).failures) addSession(p, info, protocol, session)
|
||||
}
|
||||
sendSoon(context)
|
||||
}
|
||||
|
||||
private fun addSession(
|
||||
p: android.content.SharedPreferences,
|
||||
info: ServiceDiscovery.HeadUnitInfo?,
|
||||
protocol: String,
|
||||
session: JSONObject,
|
||||
) {
|
||||
val sessions = runCatching { JSONArray(p.getString(KEY_SESSIONS, "[]")) }.getOrDefault(JSONArray())
|
||||
if (sessions.length() >= MAX_SESSIONS) return
|
||||
// The same car fields as the cars list, so the dashboard can line the two up.
|
||||
session.put("name", info?.headUnitName.orEmpty())
|
||||
.put("car", info?.carModel.orEmpty())
|
||||
.put("year", info?.carYear.orEmpty())
|
||||
.put("protocol", protocol)
|
||||
sessions.put(session)
|
||||
// Written at once: after a crash the app is about to die, and a deferred write would be lost.
|
||||
p.edit().putString(KEY_SESSIONS, sessions.toString()).commit()
|
||||
}
|
||||
|
||||
private fun addCar(
|
||||
p: android.content.SharedPreferences,
|
||||
info: ServiceDiscovery.HeadUnitInfo?,
|
||||
@@ -162,7 +199,7 @@ object UsageStats {
|
||||
// The same car and outcome twice in a day is one fact, not two.
|
||||
val seen = (0 until cars.length()).any { cars.optJSONObject(it)?.toString() == car.toString() }
|
||||
if (!seen && cars.length() < MAX_CARS) cars.put(car)
|
||||
p.edit().putString(KEY_CARS, cars.toString()).apply()
|
||||
p.edit().putString(KEY_CARS, cars.toString()).commit()
|
||||
}
|
||||
|
||||
// GrapheneOS reports the same build fields as stock Pixel firmware; its own apps give it away.
|
||||
@@ -203,9 +240,33 @@ object UsageStats {
|
||||
}
|
||||
note.put("cars", cars)
|
||||
}
|
||||
if (c.failures) {
|
||||
var sessions = runCatching { JSONArray(p.getString(KEY_SESSIONS, "[]")) }.getOrDefault(JSONArray())
|
||||
if (everything && sessions.length() == 0) {
|
||||
sessions = runCatching { JSONArray(p.getString(KEY_LAST_SESSIONS, "[]")) }.getOrDefault(JSONArray())
|
||||
}
|
||||
note.put("sessions", sessions)
|
||||
}
|
||||
return note
|
||||
}
|
||||
|
||||
/**
|
||||
* A made-up session, for "What gets sent" to show before the first real one: a 25-minute
|
||||
* drive that ended with the car switched off.
|
||||
*/
|
||||
val exampleSession: JSONObject
|
||||
get() = JSONObject()
|
||||
.put("stage", "streaming")
|
||||
.put("ended", "byebye")
|
||||
.put("code", "bye 1")
|
||||
.put("failed_at", 1500)
|
||||
.put("seconds", 1500)
|
||||
.put("cert", "downloaded")
|
||||
.put("name", "Head unit name")
|
||||
.put("car", "Car model")
|
||||
.put("year", "2021")
|
||||
.put("protocol", "1.5")
|
||||
|
||||
/** Sends the note now; false when it didn't get through. */
|
||||
private fun send(context: Context): Boolean {
|
||||
if (!sends || !enabled(context)) return true
|
||||
@@ -213,6 +274,7 @@ object UsageStats {
|
||||
val p = prefs(context)
|
||||
val note = synchronized(carsLock) { preview(context) }
|
||||
val sentCars = note.optJSONArray("cars")?.toString()
|
||||
val sentSessions = note.optJSONArray("sessions")?.toString()
|
||||
val sentDetails = if (note.has("android") || note.has("phone")) details(context, choices(context)).toString() else null
|
||||
val body = note.toString().toByteArray()
|
||||
return runCatching {
|
||||
@@ -234,6 +296,16 @@ object UsageStats {
|
||||
if (sentDetails != null) edit.putString(KEY_SENT_DETAILS, sentDetails).putString(KEY_DETAILS_DAY, LocalDate.now().toString())
|
||||
if (sentCars != null && sentCars != "[]") edit.putString(KEY_LAST_CARS, sentCars)
|
||||
if (sentCars == null || p.getString(KEY_CARS, "[]") == sentCars) edit.remove(KEY_CARS)
|
||||
if (sentSessions != null && sentSessions != "[]") edit.putString(KEY_LAST_SESSIONS, sentSessions)
|
||||
if (sentSessions == null || p.getString(KEY_SESSIONS, "[]") == sentSessions) {
|
||||
edit.remove(KEY_SESSIONS)
|
||||
} else if (sentSessions != "[]") {
|
||||
// A session ended mid-send: keep only the ones this note didn't carry.
|
||||
val now = JSONArray(p.getString(KEY_SESSIONS, "[]"))
|
||||
val sent = JSONArray(sentSessions).length()
|
||||
val left = JSONArray().also { for (i in sent until now.length()) it.put(now.get(i)) }
|
||||
edit.putString(KEY_SESSIONS, left.toString())
|
||||
}
|
||||
edit.apply()
|
||||
}
|
||||
GearslipLog.i("stats: note sent")
|
||||
|
||||
@@ -106,6 +106,10 @@ internal fun ColumnScope.UsageStatsRows() {
|
||||
choices = choices.copy(cars = it)
|
||||
save()
|
||||
}
|
||||
Choice("How each drive went: how far the connection got, how long it lasted, and how it ended, crashes included", choices.failures) {
|
||||
choices = choices.copy(failures = it)
|
||||
save()
|
||||
}
|
||||
SettingsDivider()
|
||||
SettingsRow(
|
||||
"What gets sent",
|
||||
@@ -122,7 +126,14 @@ internal fun ColumnScope.UsageStatsRows() {
|
||||
)
|
||||
AnimatedVisibility(noteOpen) {
|
||||
// Rebuilt from the saved choices each time one changes, so it always matches.
|
||||
val note = remember(choices) { UsageStats.preview(context, everything = true).toString(2) }
|
||||
val note = remember(choices) {
|
||||
val preview = UsageStats.preview(context, everything = true)
|
||||
// Until the first drive the list is empty, so show what one entry looks like.
|
||||
val example = if (preview.optJSONArray("sessions")?.length() == 0) {
|
||||
"\n\nExample, not sent: one drive adds this to \"sessions\"\n" + UsageStats.exampleSession.toString(2)
|
||||
} else ""
|
||||
preview.toString(2) + example
|
||||
}
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceContainerLowest,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
|
||||
Reference in new issue
Block a user