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.w("<- ByeByeRequest: reason=$reason ($name) in state $state")
|
||||||
log.hex(" byebye raw", body)
|
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)
|
send(MSG_BYEBYE_RESPONSE, ByteArray(0), encrypted = true)
|
||||||
log.i("-> ByeByeResponse")
|
log.i("-> ByeByeResponse")
|
||||||
}
|
}
|
||||||
@@ -316,7 +316,7 @@ class GearslipRunner(
|
|||||||
sendServiceDiscoveryRequest()
|
sendServiceDiscoveryRequest()
|
||||||
}
|
}
|
||||||
-2 -> {
|
-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(
|
log.verdict(
|
||||||
"NOT VIABLE (STATUS_CERTIFICATE_ERROR)",
|
"NOT VIABLE (STATUS_CERTIFICATE_ERROR)",
|
||||||
"The head unit explicitly rejected the phone's self-signed certificate. " +
|
"The head unit explicitly rejected the phone's self-signed certificate. " +
|
||||||
@@ -330,7 +330,7 @@ class GearslipRunner(
|
|||||||
state = State.DONE
|
state = State.DONE
|
||||||
}
|
}
|
||||||
-3 -> {
|
-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(
|
log.verdict(
|
||||||
"NOT VIABLE (STATUS_AUTHENTICATION_FAILURE)",
|
"NOT VIABLE (STATUS_AUTHENTICATION_FAILURE)",
|
||||||
"Authentication rejected. Broader than a pure certificate error, but the " +
|
"Authentication rejected. Broader than a pure certificate error, but the " +
|
||||||
@@ -343,7 +343,7 @@ class GearslipRunner(
|
|||||||
state = State.DONE
|
state = State.DONE
|
||||||
}
|
}
|
||||||
else -> {
|
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(
|
log.verdict(
|
||||||
"INCONCLUSIVE (AuthComplete status=$status)",
|
"INCONCLUSIVE (AuthComplete status=$status)",
|
||||||
"Unexpected status. Check MessageStatus.proto in aasdk for the meaning " +
|
"Unexpected status. Check MessageStatus.proto in aasdk for the meaning " +
|
||||||
@@ -411,7 +411,7 @@ class GearslipRunner(
|
|||||||
val video = ServiceDiscovery.findVideoService(serviceDiscoveryResponse)
|
val video = ServiceDiscovery.findVideoService(serviceDiscoveryResponse)
|
||||||
if (video == null) {
|
if (video == null) {
|
||||||
log.w("no video service in the discovery response - cannot start Phase A")
|
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
|
return
|
||||||
}
|
}
|
||||||
videoChannelId = video.serviceId
|
videoChannelId = video.serviceId
|
||||||
@@ -1101,10 +1101,10 @@ class GearslipRunner(
|
|||||||
|
|
||||||
private fun reportDisconnect() {
|
private fun reportDisconnect() {
|
||||||
when (state) {
|
when (state) {
|
||||||
State.WAIT_VERSION -> SessionReport.fail(SessionReport.Category.NO_HANDSHAKE, "no VersionRequest arrived", 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)
|
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)
|
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)
|
State.WAIT_SDR -> SessionReport.fail(SessionReport.Category.SERVICE_DISCOVERY, "accepted, then dropped before service discovery", state.name, "closed")
|
||||||
State.DONE -> {}
|
State.DONE -> {}
|
||||||
}
|
}
|
||||||
when (state) {
|
when (state) {
|
||||||
|
|||||||
@@ -29,12 +29,30 @@ object SessionReport {
|
|||||||
UNKNOWN("unclassified"),
|
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 appVersion = "?"
|
||||||
private var appCode = 0L
|
private var appCode = 0L
|
||||||
|
|
||||||
@Volatile private var startedAt = 0L
|
@Volatile private var startedAt = 0L
|
||||||
@Volatile var category = Category.NONE; private set
|
@Volatile var category = Category.NONE; private set
|
||||||
@Volatile private var failure = ""
|
@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 protocolVersion = ""
|
||||||
@Volatile private var certSource = ""
|
@Volatile private var certSource = ""
|
||||||
@Volatile private var certIssuer = ""
|
@Volatile private var certIssuer = ""
|
||||||
@@ -62,6 +80,7 @@ object SessionReport {
|
|||||||
startedAt = System.currentTimeMillis()
|
startedAt = System.currentTimeMillis()
|
||||||
category = Category.NONE
|
category = Category.NONE
|
||||||
failure = ""; protocolVersion = ""; certSource = ""; certIssuer = ""
|
failure = ""; protocolVersion = ""; certSource = ""; certIssuer = ""
|
||||||
|
stage = Stage.USB; failCode = ""; failedAfter = 0
|
||||||
tlsProtocol = ""; tlsCipher = ""; authStatus = null
|
tlsProtocol = ""; tlsCipher = ""; authStatus = null
|
||||||
headUnitInfo = null
|
headUnitInfo = null
|
||||||
carScreen = ""; carDpi = 0
|
carScreen = ""; carDpi = 0
|
||||||
@@ -69,34 +88,87 @@ object SessionReport {
|
|||||||
printed = false
|
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 certificate(source: String, issuer: String) { certSource = source; certIssuer = issuer }
|
||||||
fun tls(protocol: String, cipher: String) { tlsProtocol = protocol; tlsCipher = cipher }
|
fun tls(protocol: String, cipher: String) { tlsProtocol = protocol; tlsCipher = cipher; reached(Stage.TLS) }
|
||||||
fun auth(status: Int) { authStatus = status }
|
fun auth(status: Int) { authStatus = status; if (status == 0) reached(Stage.ACCEPTED) }
|
||||||
fun headUnit(info: String) { headUnit = info }
|
fun headUnit(info: String) { headUnit = info }
|
||||||
|
|
||||||
/** Who the car said it is, for the opt-in usage note; null until service discovery. */
|
/** Who the car said it is, for the opt-in usage note; null until service discovery. */
|
||||||
@Volatile var headUnitInfo: ServiceDiscovery.HeadUnitInfo? = null
|
@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. */
|
/** 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 carScreen = ""; private set
|
||||||
@Volatile var carDpi = 0; 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 profile(name: String?) { profile = name ?: "none matched" }
|
||||||
fun video(summary: String) { video = summary }
|
fun video(summary: String) { video = summary }
|
||||||
fun audio(summary: String) { audio = 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
|
if (this.category != Category.NONE) return
|
||||||
this.category = category
|
this.category = category
|
||||||
failure = detail
|
failure = detail
|
||||||
lastState = state
|
lastState = state
|
||||||
|
failCode = code
|
||||||
|
failedAfter = if (startedAt == 0L) 0 else (System.currentTimeMillis() - startedAt) / 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fail(category: Category, detail: String, t: Throwable, state: String = "") =
|
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. */
|
/** The report as plain text, for the log file and for anything that gets sent. */
|
||||||
fun render(): String {
|
fun render(): String {
|
||||||
@@ -145,7 +217,9 @@ object SessionReport {
|
|||||||
category == Category.NONE -> "ended before video"
|
category == Category.NONE -> "ended before video"
|
||||||
else -> category.label
|
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) }
|
runCatching { app.seb3thehacker.gearslip.update.UpdateChecker.onDriveEnded(context) }
|
||||||
}
|
}
|
||||||
GearslipLog.flush()
|
GearslipLog.flush()
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ object UsageStats {
|
|||||||
val android: Boolean = true,
|
val android: Boolean = true,
|
||||||
val phone: Boolean = true,
|
val phone: Boolean = true,
|
||||||
val cars: 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"
|
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. */
|
/** The daily background job of earlier builds, cancelled wherever it's still scheduled. */
|
||||||
private const val OLD_JOB_ID = 4_207
|
private const val OLD_JOB_ID = 4_207
|
||||||
private const val MAX_CARS = 10
|
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)
|
private fun prefs(context: Context) = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
@@ -69,6 +75,7 @@ object UsageStats {
|
|||||||
android = p.getBoolean("share_android", true),
|
android = p.getBoolean("share_android", true),
|
||||||
phone = p.getBoolean("share_phone", true),
|
phone = p.getBoolean("share_phone", true),
|
||||||
cars = p.getBoolean("share_cars", 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_android", choices.android)
|
||||||
.putBoolean("share_phone", choices.phone)
|
.putBoolean("share_phone", choices.phone)
|
||||||
.putBoolean("share_cars", choices.cars)
|
.putBoolean("share_cars", choices.cars)
|
||||||
|
.putBoolean("share_failures", choices.failures)
|
||||||
.apply()
|
.apply()
|
||||||
// Turning it on sends nothing yet: the driver may still be ticking boxes. The first note
|
// Turning it on sends nothing yet: the driver may still be ticking boxes. The first note
|
||||||
// waits for [choicesClosed].
|
// 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. */
|
/** 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)
|
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) {
|
fun onAppOpened(context: Context) {
|
||||||
context.getSystemService(JobScheduler::class.java)?.cancel(OLD_JOB_ID)
|
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)
|
private val sending = AtomicBoolean(false)
|
||||||
@@ -134,13 +149,35 @@ object UsageStats {
|
|||||||
outcome: String,
|
outcome: String,
|
||||||
screen: String = "",
|
screen: String = "",
|
||||||
dpi: Int = 0,
|
dpi: Int = 0,
|
||||||
|
session: JSONObject? = null,
|
||||||
) {
|
) {
|
||||||
if (!enabled(context)) return
|
if (!enabled(context)) return
|
||||||
val p = prefs(context)
|
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)
|
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(
|
private fun addCar(
|
||||||
p: android.content.SharedPreferences,
|
p: android.content.SharedPreferences,
|
||||||
info: ServiceDiscovery.HeadUnitInfo?,
|
info: ServiceDiscovery.HeadUnitInfo?,
|
||||||
@@ -162,7 +199,7 @@ object UsageStats {
|
|||||||
// The same car and outcome twice in a day is one fact, not two.
|
// 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() }
|
val seen = (0 until cars.length()).any { cars.optJSONObject(it)?.toString() == car.toString() }
|
||||||
if (!seen && cars.length() < MAX_CARS) cars.put(car)
|
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.
|
// 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)
|
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
|
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. */
|
/** Sends the note now; false when it didn't get through. */
|
||||||
private fun send(context: Context): Boolean {
|
private fun send(context: Context): Boolean {
|
||||||
if (!sends || !enabled(context)) return true
|
if (!sends || !enabled(context)) return true
|
||||||
@@ -213,6 +274,7 @@ object UsageStats {
|
|||||||
val p = prefs(context)
|
val p = prefs(context)
|
||||||
val note = synchronized(carsLock) { preview(context) }
|
val note = synchronized(carsLock) { preview(context) }
|
||||||
val sentCars = note.optJSONArray("cars")?.toString()
|
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 sentDetails = if (note.has("android") || note.has("phone")) details(context, choices(context)).toString() else null
|
||||||
val body = note.toString().toByteArray()
|
val body = note.toString().toByteArray()
|
||||||
return runCatching {
|
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 (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 && sentCars != "[]") edit.putString(KEY_LAST_CARS, sentCars)
|
||||||
if (sentCars == null || p.getString(KEY_CARS, "[]") == sentCars) edit.remove(KEY_CARS)
|
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()
|
edit.apply()
|
||||||
}
|
}
|
||||||
GearslipLog.i("stats: note sent")
|
GearslipLog.i("stats: note sent")
|
||||||
|
|||||||
@@ -106,6 +106,10 @@ internal fun ColumnScope.UsageStatsRows() {
|
|||||||
choices = choices.copy(cars = it)
|
choices = choices.copy(cars = it)
|
||||||
save()
|
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()
|
SettingsDivider()
|
||||||
SettingsRow(
|
SettingsRow(
|
||||||
"What gets sent",
|
"What gets sent",
|
||||||
@@ -122,7 +126,14 @@ internal fun ColumnScope.UsageStatsRows() {
|
|||||||
)
|
)
|
||||||
AnimatedVisibility(noteOpen) {
|
AnimatedVisibility(noteOpen) {
|
||||||
// Rebuilt from the saved choices each time one changes, so it always matches.
|
// 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(
|
Surface(
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerLowest,
|
color = MaterialTheme.colorScheme.surfaceContainerLowest,
|
||||||
shape = MaterialTheme.shapes.medium,
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
|||||||
Reference in new issue
Block a user