mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 00:12:24 +01:00
car: calls show on the car's own screens, and its phone button answers
This commit is contained in:
1 parent
d1f67bb64d
commit
f4b8391fa3
3 files changed
+133
No files matched your search
@@ -96,6 +96,7 @@ class GearslipRunner(
|
|||||||
@Volatile private var inputKeycodes: List<Int> = emptyList()
|
@Volatile private var inputKeycodes: List<Int> = emptyList()
|
||||||
@Volatile private var sensorChannelId: Int = -1
|
@Volatile private var sensorChannelId: Int = -1
|
||||||
@Volatile private var mediaStatus: CarMediaStatus? = null
|
@Volatile private var mediaStatus: CarMediaStatus? = null
|
||||||
|
@Volatile private var phoneStatus: app.seb3thehacker.gearslip.call.CarPhoneStatus? = null
|
||||||
@Volatile private var clusterNav: ClusterNav? = null
|
@Volatile private var clusterNav: ClusterNav? = null
|
||||||
private var sensorTypes: List<Int> = emptyList()
|
private var sensorTypes: List<Int> = emptyList()
|
||||||
@Volatile private var audioLink: AudioLink? = null
|
@Volatile private var audioLink: AudioLink? = null
|
||||||
@@ -190,6 +191,8 @@ class GearslipRunner(
|
|||||||
LocationCheck.onCarDisconnected()
|
LocationCheck.onCarDisconnected()
|
||||||
mediaStatus?.stop()
|
mediaStatus?.stop()
|
||||||
mediaStatus = null
|
mediaStatus = null
|
||||||
|
phoneStatus?.stop()
|
||||||
|
phoneStatus = null
|
||||||
clusterNav?.stop()
|
clusterNav?.stop()
|
||||||
clusterNav = null
|
clusterNav = null
|
||||||
lastSensorNight = null
|
lastSensorNight = null
|
||||||
@@ -237,6 +240,8 @@ class GearslipRunner(
|
|||||||
clusterNav?.onMessage(messageId, body)
|
clusterNav?.onMessage(messageId, body)
|
||||||
} else if (frame.channel == mediaStatus?.channelId) {
|
} else if (frame.channel == mediaStatus?.channelId) {
|
||||||
mediaStatus?.onMessage(messageId, body)
|
mediaStatus?.onMessage(messageId, body)
|
||||||
|
} else if (frame.channel == phoneStatus?.channelId) {
|
||||||
|
phoneStatus?.onMessage(messageId, body)
|
||||||
} else if (frame.channel == guidanceLink?.channelId) {
|
} else if (frame.channel == guidanceLink?.channelId) {
|
||||||
guidanceLink?.onMessage(messageId, body)
|
guidanceLink?.onMessage(messageId, body)
|
||||||
} else if (frame.channel == CarBluetooth.channelId) {
|
} else if (frame.channel == CarBluetooth.channelId) {
|
||||||
@@ -512,6 +517,7 @@ class GearslipRunner(
|
|||||||
startSensorChannel(serviceDiscoveryResponse)
|
startSensorChannel(serviceDiscoveryResponse)
|
||||||
startMicChannel(serviceDiscoveryResponse)
|
startMicChannel(serviceDiscoveryResponse)
|
||||||
startMediaStatusChannel(serviceDiscoveryResponse)
|
startMediaStatusChannel(serviceDiscoveryResponse)
|
||||||
|
startPhoneStatusChannel(serviceDiscoveryResponse)
|
||||||
startClusterNavChannel(serviceDiscoveryResponse)
|
startClusterNavChannel(serviceDiscoveryResponse)
|
||||||
startBluetooth(serviceDiscoveryResponse)
|
startBluetooth(serviceDiscoveryResponse)
|
||||||
startVendorChannel(serviceDiscoveryResponse)
|
startVendorChannel(serviceDiscoveryResponse)
|
||||||
@@ -628,6 +634,19 @@ class GearslipRunner(
|
|||||||
openChannel(channel)
|
openChannel(channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Calls, for the car's own screens: the cluster, the steering wheel display. */
|
||||||
|
private fun startPhoneStatusChannel(serviceDiscoveryResponse: ByteArray) {
|
||||||
|
val channel = ServiceDiscovery.findPhoneStatusChannel(serviceDiscoveryResponse) ?: run {
|
||||||
|
log.i("no phone status channel - the car's own screens won't show calls")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.i("phone status channel: $channel")
|
||||||
|
phoneStatus = app.seb3thehacker.gearslip.call.CarPhoneStatus(channel) { id, body ->
|
||||||
|
send(id, body, encrypted = true, channel = channel)
|
||||||
|
}
|
||||||
|
openChannel(channel)
|
||||||
|
}
|
||||||
|
|
||||||
/** Bluetooth pairing, for calls: the channel only opens if the driver asks to pair. */
|
/** Bluetooth pairing, for calls: the channel only opens if the driver asks to pair. */
|
||||||
/**
|
/**
|
||||||
* GM's own channel ("com.gm.vip"), opened in GM cars only. Issue #18: a 2021 Bolt ends every
|
* GM's own channel ("com.gm.vip"), opened in GM cars only. Issue #18: a 2021 Bolt ends every
|
||||||
|
|||||||
@@ -116,6 +116,12 @@ object ServiceDiscovery {
|
|||||||
*/
|
*/
|
||||||
fun findMediaStatusChannel(response: ByteArray): Int? = findChannelWith(response, 9)
|
fun findMediaStatusChannel(response: ByteArray): Int? = findChannelWith(response, 9)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The channel the car takes call status on (PhoneStatusService, Service field 10), for its
|
||||||
|
* cluster and phone screens. It carries no settings. Null when the car has none.
|
||||||
|
*/
|
||||||
|
fun findPhoneStatusChannel(response: ByteArray): Int? = findChannelWith(response, 10)
|
||||||
|
|
||||||
class NavStatusService(val serviceId: Int, val minimumIntervalMs: Int)
|
class NavStatusService(val serviceId: Int, val minimumIntervalMs: Int)
|
||||||
|
|
||||||
/** The cluster advertises its minimum update interval in NavigationStatusService field 1. */
|
/** The cluster advertises its minimum update interval in NavigationStatusService field 1. */
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package app.seb3thehacker.gearslip.call
|
||||||
|
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import app.seb3thehacker.gearslip.GearslipLog
|
||||||
|
import app.seb3thehacker.gearslip.Protobuf
|
||||||
|
import app.seb3thehacker.gearslip.Wire
|
||||||
|
import app.seb3thehacker.gearslip.car.CarKeys
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells the car about calls, so its own screens show them: the instrument cluster, the steering
|
||||||
|
* wheel display, the car's phone page. Android Auto does the same over the car's
|
||||||
|
* PhoneStatusService (Service field 10):
|
||||||
|
*
|
||||||
|
* -> PhoneStatusUpdate { repeated PhoneCall calls = 1 }
|
||||||
|
* PhoneCall { state = 1 (1 in call, 4 incoming, 6 muted); duration_seconds = 2;
|
||||||
|
* number = 3; name = 4 }
|
||||||
|
* <- PhoneStatusInput { input = 1 { action = 1 (7 call) }; caller = 2; name = 3 }
|
||||||
|
*
|
||||||
|
* Read once a second from [CarCalls]. An update goes when the call changes, and every second
|
||||||
|
* while one is going, so the car's timer counts along; no calls goes once, as an empty list.
|
||||||
|
* The car's own call button answers through [CarKeys], like the steering wheel's.
|
||||||
|
*/
|
||||||
|
class CarPhoneStatus(
|
||||||
|
val channelId: Int,
|
||||||
|
private val sendOnChannel: (messageId: Int, body: ByteArray) -> Unit,
|
||||||
|
) {
|
||||||
|
@Volatile private var running = false
|
||||||
|
private var sent: String? = null
|
||||||
|
|
||||||
|
fun onMessage(messageId: Int, body: ByteArray) {
|
||||||
|
when (messageId) {
|
||||||
|
MSG_CHANNEL_OPEN_RESPONSE -> {
|
||||||
|
val status = Protobuf.readInt32Field(body, 1)
|
||||||
|
GearslipLog.i("phone status: <- ChannelOpenResponse: status=$status")
|
||||||
|
if (status == 0) start()
|
||||||
|
}
|
||||||
|
MSG_INPUT -> onInput(body)
|
||||||
|
else -> GearslipLog.i("phone status: <- unhandled message id=$messageId (${body.size} bytes)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun start() {
|
||||||
|
if (running) return
|
||||||
|
running = true
|
||||||
|
Thread({
|
||||||
|
while (running) {
|
||||||
|
runCatching { tick() }.onFailure { GearslipLog.w("phone status: ${it.message}") }
|
||||||
|
Thread.sleep(TICK_MS)
|
||||||
|
}
|
||||||
|
}, "gearslip-phone-status").start()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
running = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tick() {
|
||||||
|
val call = when (val state = CarCalls.state.value) {
|
||||||
|
CallUiState.None -> null
|
||||||
|
is CallUiState.Ringing -> Call(STATE_INCOMING, 0, state.caller)
|
||||||
|
is CallUiState.Active -> Call(
|
||||||
|
if (CarCalls.muted.value) STATE_MUTED else STATE_IN_CALL,
|
||||||
|
((SystemClock.uptimeMillis() - state.since) / 1000).coerceAtLeast(0),
|
||||||
|
state.caller,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Only a call that's going changes from one second to the next.
|
||||||
|
val key = call?.let { "${it.state}|${it.caller?.number}|${it.caller?.name}|${if (it.state == STATE_INCOMING) 0 else it.seconds}" } ?: ""
|
||||||
|
if (key == sent) return
|
||||||
|
val changed = call?.state != lastState
|
||||||
|
sent = key
|
||||||
|
lastState = call?.state
|
||||||
|
val body = call?.let { Protobuf.bytesField(1, it.encode()) } ?: ByteArray(0)
|
||||||
|
sendOnChannel(MSG_STATUS, body)
|
||||||
|
if (changed) GearslipLog.i("phone status: -> ${call?.let { STATE_NAMES[it.state] } ?: "no calls"}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lastState: Int? = null
|
||||||
|
|
||||||
|
private class Call(val state: Int, val seconds: Long, val caller: Caller?) {
|
||||||
|
fun encode(): ByteArray {
|
||||||
|
var body = Protobuf.varintField(1, state.toLong()) + Protobuf.varintField(2, seconds)
|
||||||
|
caller?.number?.takeIf { it.isNotBlank() }?.let { body += Protobuf.stringField(3, it) }
|
||||||
|
caller?.name?.takeIf { it.isNotBlank() }?.let { body += Protobuf.stringField(4, it) }
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The car's own phone buttons. Only "call" does anything: it answers, as the wheel's does. */
|
||||||
|
private fun onInput(body: ByteArray) {
|
||||||
|
val action = Wire.bytes(Wire.fields(body), 1)?.let { Wire.varint(Wire.fields(it), 1)?.toInt() }
|
||||||
|
GearslipLog.i("phone status: <- input action=${action ?: "?"}")
|
||||||
|
if (action == ACTION_CALL) CarKeys.onKey(KeyEvent.KEYCODE_CALL, down = true, longPress = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TICK_MS = 1_000L
|
||||||
|
private const val MSG_CHANNEL_OPEN_RESPONSE = 8
|
||||||
|
private const val MSG_STATUS = 32769 // 0x8001
|
||||||
|
private const val MSG_INPUT = 32770 // 0x8002
|
||||||
|
private const val STATE_IN_CALL = 1
|
||||||
|
private const val STATE_INCOMING = 4
|
||||||
|
private const val STATE_MUTED = 6
|
||||||
|
private const val ACTION_CALL = 7
|
||||||
|
private val STATE_NAMES = mapOf(STATE_IN_CALL to "in a call", STATE_INCOMING to "ringing", STATE_MUTED to "in a call, muted")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user