fix LIVI not connecting

The head unit speaks first and gave up while the phone spent a second unlocking its
certificate before reading. Read straight away and unlock the certificate alongside it,
and keep it unlocked between connections.
This commit is contained in:
Seb3thehacker committed 2026-10-04 14:07:35 -07:00
1 parent c6b7f0e178
commit 1341b4a08a
3 files changed
+57 -11

No files matched your search

@@ -47,7 +47,10 @@ object CertProvider {
val kind: Kind = Kind.SELF_SIGNED, val kind: Kind = Kind.SELF_SIGNED,
) )
/** Loaded fresh each call, so a newly imported or pushed cert takes effect on the next connect. */ /**
* The identity for the next connection. A newly imported or pushed cert takes effect on the
* next connect: see [loadFromFiles] for how the cache notices.
*/
fun load(context: Context): Identity = loadFromFiles(context) ?: run { fun load(context: Context): Identity = loadFromFiles(context) ?: run {
val generated = SelfSignedCert.generate() val generated = SelfSignedCert.generate()
Identity( Identity(
@@ -65,7 +68,38 @@ object CertProvider {
*/ */
fun loadSupplied(context: Context): Identity? = loadFromFiles(context) fun loadSupplied(context: Context): Identity? = loadFromFiles(context)
/**
* Unlocking a PKCS#12 file takes about a second on a Pixel 6, which a head unit waiting for
* its first reply may not wait out. So the unlocked identity is kept, keyed on the files it
* could come from: importing, downloading, removing or pushing a cert changes the key.
*/
@Volatile private var cached: Pair<String, Identity>? = null
private fun sourcesKey(context: Context): String {
val dir = context.getExternalFilesDir(null)
val files = listOf(
importedFile(context, P12_NAME),
downloadedFile(context, P12_NAME),
dir?.let { File(it, P12_NAME) },
dir?.let { File(it, PASS_NAME) },
)
return files.joinToString("|") { f -> f?.let { "${it.path}:${it.lastModified()}:${it.length()}" } ?: "-" }
}
/** Unlocks the certificate ahead of time, so the first connection doesn't wait on it. */
fun warm(context: Context) {
runCatching { loadFromFiles(context) }
}
private fun loadFromFiles(context: Context): Identity? { private fun loadFromFiles(context: Context): Identity? {
val key = sourcesKey(context)
cached?.let { (k, identity) -> if (k == key) return identity }
val identity = unlockFromFiles(context) ?: return null
cached = key to identity
return identity
}
private fun unlockFromFiles(context: Context): Identity? {
val imported = importedFile(context, P12_NAME) val imported = importedFile(context, P12_NAME)
if (imported.isFile) { if (imported.isFile) {
runCatching { runCatching {
@@ -10,6 +10,7 @@ import app.seb3thehacker.gearslip.car.CarServices
import app.seb3thehacker.gearslip.car.CarSettings import app.seb3thehacker.gearslip.car.CarSettings
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.util.concurrent.FutureTask
/** /**
* Drives the phone side of the control-channel handshake far enough to answer one question: * Drives the phone side of the control-channel handshake far enough to answer one question:
@@ -38,7 +39,10 @@ class GearslipRunner(
private val parser = Frames.Parser() private val parser = Frames.Parser()
private val assembler = Frames.Assembler() private val assembler = Frames.Assembler()
private lateinit var tls: PhoneTls private lateinit var identityTask: FutureTask<PhoneTls>
/** Waits for the certificate only when TLS is first needed; see [run]. */
private val tls: PhoneTls by lazy { identityTask.get() }
@Volatile private var state = State.WAIT_VERSION @Volatile private var state = State.WAIT_VERSION
@Volatile private var lastProgress = System.currentTimeMillis() @Volatile private var lastProgress = System.currentTimeMillis()
@@ -76,14 +80,19 @@ class GearslipRunner(
SessionStatus.connecting("Connected to the head unit") SessionStatus.connecting("Connected to the head unit")
log.i("device=${Build.MANUFACTURER} ${Build.MODEL} android=${Build.VERSION.RELEASE} (sdk ${Build.VERSION.SDK_INT})") log.i("device=${Build.MANUFACTURER} ${Build.MODEL} android=${Build.VERSION.RELEASE} (sdk ${Build.VERSION.SDK_INT})")
val identity = identityProvider() // The head unit speaks first, and some give up if the phone doesn't read within about a
val cert = identity.certificate // second. The version exchange needs no certificate, so read at once and unlock the
log.i("phone certificate source: ${identity.source}") // certificate alongside; the TLS handshake waits for it.
SessionReport.certificate(identity.source.toString(), cert.issuerX500Principal.toString()) identityTask = FutureTask {
log.i(" subject = ${cert.subjectX500Principal}") val identity = identityProvider()
log.i(" issuer = ${cert.issuerX500Principal}") val cert = identity.certificate
log.i(" serial = ${cert.serialNumber} valid ${cert.notBefore}..${cert.notAfter}") log.i("phone certificate source: ${identity.source}")
tls = PhoneTls(identity.keyStore, identity.password) SessionReport.certificate(identity.source.toString(), cert.issuerX500Principal.toString())
log.i(" subject = ${cert.subjectX500Principal}")
log.i(" issuer = ${cert.issuerX500Principal}")
log.i(" serial = ${cert.serialNumber} valid ${cert.notBefore}..${cert.notAfter}")
PhoneTls(identity.keyStore, identity.password)
}.also { Thread(it, "gearslip-identity").start() }
startWatchdog() startWatchdog()
@@ -1,13 +1,15 @@
package app.seb3thehacker.gearslip.car package app.seb3thehacker.gearslip.car
import android.content.Context import android.content.Context
import app.seb3thehacker.gearslip.CertProvider
import app.seb3thehacker.gearslip.GearslipLog import app.seb3thehacker.gearslip.GearslipLog
import app.seb3thehacker.gearslip.mirror.PhoneApps import app.seb3thehacker.gearslip.mirror.PhoneApps
import java.util.concurrent.Executors import java.util.concurrent.Executors
/** /**
* Loads what the car screens show before anyone opens them: the car and media apps with their * Loads what the car screens show before anyone opens them: the car and media apps with their
* icons, the phone's launcher apps, the weather and the calendar. Each screen paints from these * icons, the phone's launcher apps, the weather and the calendar. Also unlocks the certificate,
* so a head unit's first message is answered at once. Each screen paints from these
* results at once and refreshes behind them, so nothing waits on the package manager, the * results at once and refreshes behind them, so nothing waits on the package manager, the
* network or the calendar provider. * network or the calendar provider.
* *
@@ -28,6 +30,7 @@ object Prefetch {
val app = context.applicationContext val app = context.applicationContext
worker.execute { worker.execute {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
step("certificate") { CertProvider.warm(app) }
step("launcher apps") { warmLauncherApps(app) } step("launcher apps") { warmLauncherApps(app) }
step("phone apps") { PhoneApps.warm(app) } step("phone apps") { PhoneApps.warm(app) }
step("weather and calendar") { warmDashboard(app) } step("weather and calendar") { warmDashboard(app) }