mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
A car that keeps its screen until it knows the phone hears its Bluetooth address
The 2020 Civic (issue #43) answers every request for its screen with its own, and waits to hear which phone is connected over the car's Bluetooth channel. Gearslip never told it: CarBluetooth was only set up once the car screen drew, so on a car that never shows it the typed address never left the phone. It's now set up when Gearslip starts. Once the car hears the address it sends Play while still on its own screen, and reaching for the media app before the car UI exists ended the session; such keys are now logged and ignored. The second request for the screen now runs whether or not the media channel is open, and a car that keeps its screen gets the Bluetooth channel opened, and pairing started, as a real phone does. When the car answers there, it is asked for its screen again.
This commit is contained in:
1 parent
c389c1c586
commit
73ffa7925e
4 files changed
+98
-18
No files matched your search
@@ -159,6 +159,9 @@ class GearslipActivity : ComponentActivity(), Projection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
CarSettings.init(this)
|
CarSettings.init(this)
|
||||||
|
// Before any car connects: CarServices only starts once the car shows Gearslip's screen,
|
||||||
|
// and a car that waits to know the phone before it shows it would never hear (issue #43).
|
||||||
|
app.seb3thehacker.gearslip.car.CarBluetooth.init(this)
|
||||||
AppSettings.applyGearslipEnabled(this)
|
AppSettings.applyGearslipEnabled(this)
|
||||||
// A capture that died without tidying up would leave notifications exposed to every
|
// A capture that died without tidying up would leave notifications exposed to every
|
||||||
// later screen share. Nothing is capturing if the service is not running, so undo it.
|
// later screen share. Nothing is capturing if the service is not running, so undo it.
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ class GearslipRunner(
|
|||||||
private var selectedConfigIndex: Int? = null
|
private var selectedConfigIndex: Int? = null
|
||||||
private var vehicleProfile: VehicleProfile? = null
|
private var vehicleProfile: VehicleProfile? = null
|
||||||
@Volatile private var videoStarted = false
|
@Volatile private var videoStarted = false
|
||||||
|
/** The car's screen has been asked for once, so asking again is in order. */
|
||||||
|
@Volatile private var focusAsked = false
|
||||||
private var videoSource: VideoSource? = null
|
private var videoSource: VideoSource? = null
|
||||||
|
|
||||||
/** Between the car UI and the encoder when the car squashes the frame (see [screenShape]). */
|
/** Between the car UI and the encoder when the car squashes the frame (see [screenShape]). */
|
||||||
@@ -773,6 +775,13 @@ class GearslipRunner(
|
|||||||
bluetooth,
|
bluetooth,
|
||||||
open = { openChannel(bluetooth.serviceId) },
|
open = { openChannel(bluetooth.serviceId) },
|
||||||
send = { id, body -> send(id, body, encrypted = true, channel = bluetooth.serviceId) },
|
send = { id, body -> send(id, body, encrypted = true, channel = bluetooth.serviceId) },
|
||||||
|
// A car keeping its screen until it knows the phone may give it up now; ask again.
|
||||||
|
answered = {
|
||||||
|
if (running && focusAsked && !videoStarted) {
|
||||||
|
log.i("the car answered on Bluetooth while keeping its screen - asking for it again")
|
||||||
|
requestProjectedFocus(withDisplay = true)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1068,7 +1077,7 @@ class GearslipRunner(
|
|||||||
"(head unit offered ${offered.size}), max_unacked=$maxUnacked",
|
"(head unit offered ${offered.size}), max_unacked=$maxUnacked",
|
||||||
)
|
)
|
||||||
requestProjectedFocus()
|
requestProjectedFocus()
|
||||||
if (CarAudio.openSink != null) watchForWithheldScreen()
|
watchForWithheldScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
MSG_VIDEO_FOCUS_NOTIFICATION -> {
|
MSG_VIDEO_FOCUS_NOTIFICATION -> {
|
||||||
@@ -1361,6 +1370,7 @@ class GearslipRunner(
|
|||||||
* the field is deprecated, but a car older than that may ignore a request without it.
|
* the field is deprecated, but a car older than that may ignore a request without it.
|
||||||
*/
|
*/
|
||||||
private fun requestProjectedFocus(withDisplay: Boolean = false) {
|
private fun requestProjectedFocus(withDisplay: Boolean = false) {
|
||||||
|
focusAsked = true
|
||||||
// VideoFocusRequestNotification { disp_channel_id = 1; mode = 2; reason = 3 }. Reason 1
|
// VideoFocusRequestNotification { disp_channel_id = 1; mode = 2; reason = 3 }. Reason 1
|
||||||
// means "the phone's screen is off", which a 2020 Civic may have taken as a reason to
|
// means "the phone's screen is off", which a 2020 Civic may have taken as a reason to
|
||||||
// keep its own screen (issue #43); 0 is UNKNOWN.
|
// keep its own screen (issue #43); 0 is UNKNOWN.
|
||||||
@@ -1380,6 +1390,10 @@ class GearslipRunner(
|
|||||||
* straight over keep their own source as before.
|
* straight over keep their own source as before.
|
||||||
* The 2017 Pilot (protocol 1.0) still kept its screen with the channel open, so the second
|
* The 2017 Pilot (protocol 1.0) still kept its screen with the channel open, so the second
|
||||||
* request also names the display's channel, as cars of the Pilot's age may need.
|
* request also names the display's channel, as cars of the Pilot's age may need.
|
||||||
|
* This runs whether or not the media channel was open: a 2020 Civic with music through
|
||||||
|
* Gearslip opened it at connect, so on 0.3.40 it never heard the second request (issue #43).
|
||||||
|
* The car also hears which phone this is over Bluetooth (see [CarBluetooth.onScreenWithheld]),
|
||||||
|
* which Hondas may wait for; its answer asks for the screen once more.
|
||||||
*/
|
*/
|
||||||
private fun watchForWithheldScreen() {
|
private fun watchForWithheldScreen() {
|
||||||
Thread({
|
Thread({
|
||||||
@@ -1389,11 +1403,12 @@ class GearslipRunner(
|
|||||||
if (open != null) {
|
if (open != null) {
|
||||||
log.w("no screen from the car after ${WITHHELD_SCREEN_MS / 1000}s - opening the media channel and asking again")
|
log.w("no screen from the car after ${WITHHELD_SCREEN_MS / 1000}s - opening the media channel and asking again")
|
||||||
open()
|
open()
|
||||||
// Give the car the channel's open response before it hears the request again.
|
|
||||||
Thread.sleep(1_000)
|
|
||||||
} else {
|
} else {
|
||||||
log.w("no screen from the car after ${WITHHELD_SCREEN_MS / 1000}s - asking again")
|
log.w("no screen from the car after ${WITHHELD_SCREEN_MS / 1000}s - asking again")
|
||||||
}
|
}
|
||||||
|
val toldBluetooth = CarBluetooth.onScreenWithheld()
|
||||||
|
// Give the car the channels' open responses before it hears the request again.
|
||||||
|
if (open != null || toldBluetooth) Thread.sleep(1_000)
|
||||||
if (running && !videoStarted) requestProjectedFocus(withDisplay = true)
|
if (running && !videoStarted) requestProjectedFocus(withDisplay = true)
|
||||||
}, "gearslip-focus-fallback").apply { isDaemon = true }.start()
|
}, "gearslip-focus-fallback").apply { isDaemon = true }.start()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,11 +57,12 @@ object CarBluetooth {
|
|||||||
private val main = Handler(Looper.getMainLooper())
|
private val main = Handler(Looper.getMainLooper())
|
||||||
private var appContext: Context? = null
|
private var appContext: Context? = null
|
||||||
|
|
||||||
/** This session's car: its channel, and how to open and write to it. */
|
/** This session's car: its channel, how to open and write to it, and who hears its answers. */
|
||||||
private class Car(
|
private class Car(
|
||||||
val service: ServiceDiscovery.BluetoothService,
|
val service: ServiceDiscovery.BluetoothService,
|
||||||
val open: () -> Unit,
|
val open: () -> Unit,
|
||||||
val send: (messageId: Int, body: ByteArray) -> Unit,
|
val send: (messageId: Int, body: ByteArray) -> Unit,
|
||||||
|
val answered: () -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Volatile private var car: Car? = null
|
@Volatile private var car: Car? = null
|
||||||
@@ -104,13 +105,16 @@ object CarBluetooth {
|
|||||||
_addressWanted.value = null
|
_addressWanted.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Once per car: it waits on the phone, and the car screen says so once. */
|
/**
|
||||||
|
* Once per car: it waits on the phone, and the car screen says so once. [again] asks a car
|
||||||
|
* that was asked before, for one that keeps its screen until it knows the phone.
|
||||||
|
*/
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
private fun askForAddress(device: BluetoothDevice, carAddress: String) {
|
private fun askForAddress(device: BluetoothDevice, carAddress: String, again: Boolean = false) {
|
||||||
val context = appContext ?: return
|
val context = appContext ?: return
|
||||||
val asked = AppSettings.getString(context, KEY_ADDRESS_ASKED, "").split(",").filter { it.isNotEmpty() }
|
val asked = AppSettings.getString(context, KEY_ADDRESS_ASKED, "").split(",").filter { it.isNotEmpty() }
|
||||||
if (hash(carAddress) in asked) return
|
if (hash(carAddress) in asked && !again) return
|
||||||
AppSettings.putString(context, KEY_ADDRESS_ASKED, (asked + hash(carAddress)).joinToString(","))
|
AppSettings.putString(context, KEY_ADDRESS_ASKED, (asked + hash(carAddress)).distinct().joinToString(","))
|
||||||
val name = runCatching { device.alias ?: device.name }.getOrNull()?.takeIf { it.isNotBlank() }
|
val name = runCatching { device.alias ?: device.name }.getOrNull()?.takeIf { it.isNotBlank() }
|
||||||
?: context.getString(R.string.bt_address_your_car)
|
?: context.getString(R.string.bt_address_your_car)
|
||||||
AppSettings.putString(context, KEY_ADDRESS_ASK, name)
|
AppSettings.putString(context, KEY_ADDRESS_ASK, name)
|
||||||
@@ -119,13 +123,17 @@ object CarBluetooth {
|
|||||||
main.post { CarToasts.show(this, context.getString(R.string.bt_address_car_note), CarToast.LENGTH_LONG) }
|
main.post { CarToasts.show(this, context.getString(R.string.bt_address_car_note), CarToast.LENGTH_LONG) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Called once the car lists its services. Asks on the car screen only if there's pairing to do. */
|
/**
|
||||||
|
* Called once the car lists its services. Asks on the car screen only if there's pairing to do.
|
||||||
|
* [answered] runs when the car answers a pairing request or the pairing finishes.
|
||||||
|
*/
|
||||||
fun onCarConnected(
|
fun onCarConnected(
|
||||||
service: ServiceDiscovery.BluetoothService,
|
service: ServiceDiscovery.BluetoothService,
|
||||||
open: () -> Unit,
|
open: () -> Unit,
|
||||||
send: (messageId: Int, body: ByteArray) -> Unit,
|
send: (messageId: Int, body: ByteArray) -> Unit,
|
||||||
|
answered: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
car = Car(service, open, send)
|
car = Car(service, open, send, answered)
|
||||||
bondStarted = false
|
bondStarted = false
|
||||||
announcing = false
|
announcing = false
|
||||||
log.i("the car offers Bluetooth pairing, methods=${service.pairingMethods}")
|
log.i("the car offers Bluetooth pairing, methods=${service.pairingMethods}")
|
||||||
@@ -159,6 +167,40 @@ object CarBluetooth {
|
|||||||
c.open()
|
c.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The car is keeping its own screen: do what Android Auto does on every connection, and tell
|
||||||
|
* it over Bluetooth which phone this is. Every Honda on file keeps its screen (a 2017 Pilot, a
|
||||||
|
* 2017 and a 2020 Civic; issues #43 and #57), and in each log the car's Bluetooth channel was
|
||||||
|
* never opened. Honda starts Android Auto with what its manuals call auto pairing, which runs
|
||||||
|
* on that channel, and a 2021 Bolt kept hanging up until it heard the phone's address (#18).
|
||||||
|
*
|
||||||
|
* - Paired, with the address typed in: the car already heard it at connect.
|
||||||
|
* - Paired, no address: open the channel, and ask for the address on the phone again.
|
||||||
|
* - Not paired: the Pair card waits on the car's screen, which this car won't give up, so
|
||||||
|
* pair as if the driver had tapped it. Android's pairing dialog still asks on the phone.
|
||||||
|
*
|
||||||
|
* True when the car was sent something it may answer.
|
||||||
|
*/
|
||||||
|
fun onScreenWithheld(): Boolean {
|
||||||
|
val c = car ?: return false
|
||||||
|
if (pairing || announcing) return false
|
||||||
|
val device = device() ?: return false
|
||||||
|
return when {
|
||||||
|
device.bondState == BluetoothDevice.BOND_BONDED -> {
|
||||||
|
askForAddress(device, c.service.carAddress, again = true)
|
||||||
|
announce("the car is keeping its screen")
|
||||||
|
true
|
||||||
|
}
|
||||||
|
device.bondState != BluetoothDevice.BOND_NONE -> false
|
||||||
|
declined(c.service.carAddress) -> false
|
||||||
|
else -> {
|
||||||
|
log.i("the car is keeping its screen and isn't paired with this phone - pairing, as Android Auto does")
|
||||||
|
main.post { pair() }
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onCarDisconnected() {
|
fun onCarDisconnected() {
|
||||||
car = null
|
car = null
|
||||||
pairing = false
|
pairing = false
|
||||||
@@ -226,6 +268,7 @@ object CarBluetooth {
|
|||||||
MSG_PAIRING_RESPONSE -> {
|
MSG_PAIRING_RESPONSE -> {
|
||||||
val fields = Wire.fields(body)
|
val fields = Wire.fields(body)
|
||||||
log.i("<- PairingResponse status=${Wire.varint(fields, 1)} already_paired=${Wire.varint(fields, 2) == 1L}")
|
log.i("<- PairingResponse status=${Wire.varint(fields, 1)} already_paired=${Wire.varint(fields, 2) == 1L}")
|
||||||
|
c.answered()
|
||||||
if (!pairing) return
|
if (!pairing) return
|
||||||
// Some cars start pairing themselves once they answer; give them a moment first.
|
// Some cars start pairing themselves once they answer; give them a moment first.
|
||||||
main.postDelayed({ startBond("the car answered") }, CAR_BOND_WAIT_MS)
|
main.postDelayed({ startBond("the car answered") }, CAR_BOND_WAIT_MS)
|
||||||
@@ -260,7 +303,10 @@ object CarBluetooth {
|
|||||||
log.i("bond state $previous -> $state")
|
log.i("bond state $previous -> $state")
|
||||||
when {
|
when {
|
||||||
state == BluetoothDevice.BOND_BONDING -> bondStarted = true
|
state == BluetoothDevice.BOND_BONDING -> bondStarted = true
|
||||||
state == BluetoothDevice.BOND_BONDED -> finish(R.string.bt_paired)
|
state == BluetoothDevice.BOND_BONDED -> {
|
||||||
|
finish(R.string.bt_paired)
|
||||||
|
car?.answered?.invoke()
|
||||||
|
}
|
||||||
previous == BluetoothDevice.BOND_BONDING -> finish(R.string.bt_pair_unfinished)
|
previous == BluetoothDevice.BOND_BONDING -> finish(R.string.bt_pair_unfinished)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -285,7 +331,10 @@ object CarBluetooth {
|
|||||||
|
|
||||||
/** The car as a Bluetooth device, or null (and logged) when Gearslip can't reach Bluetooth. */
|
/** The car as a Bluetooth device, or null (and logged) when Gearslip can't reach Bluetooth. */
|
||||||
private fun device(): BluetoothDevice? {
|
private fun device(): BluetoothDevice? {
|
||||||
val context = appContext ?: return null
|
val context = appContext ?: run {
|
||||||
|
log.w("can't check pairing: Bluetooth isn't set up yet")
|
||||||
|
return null
|
||||||
|
}
|
||||||
val address = car?.service?.carAddress ?: return null
|
val address = car?.service?.carAddress ?: return null
|
||||||
if (context.checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
|
if (context.checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
|
||||||
log.i("can't check pairing: the Bluetooth permission isn't granted")
|
log.i("can't check pairing: the Bluetooth permission isn't granted")
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ object CarKeys {
|
|||||||
|
|
||||||
fun init(context: Context) { appContext = context.applicationContext }
|
fun init(context: Context) { appContext = context.applicationContext }
|
||||||
|
|
||||||
|
/** The keys that act on the media app. */
|
||||||
|
private val MEDIA_KEYS = setOf(
|
||||||
|
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_MEDIA_PAUSE,
|
||||||
|
KeyEvent.KEYCODE_MEDIA_STOP, KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_MEDIA_PREVIOUS,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Every key worth asking the head unit for. The binding request lists what the head unit
|
* Every key worth asking the head unit for. The binding request lists what the head unit
|
||||||
* offered; when it offers nothing (some units leave the list empty and send keys anyway),
|
* offered; when it offers nothing (some units leave the list empty and send keys anyway),
|
||||||
@@ -87,13 +93,20 @@ object CarKeys {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (!down) return keyCode in WANTED
|
if (!down) return keyCode in WANTED
|
||||||
val media = CarServices.media
|
// Null until the car shows Gearslip's screen. A 2020 Civic sends Play as soon as it hears
|
||||||
|
// the phone's Bluetooth address, while still on its own screen, and reaching for media
|
||||||
|
// then ended the session (issue #43).
|
||||||
|
val media = CarServices.mediaOrNull()
|
||||||
|
if (media == null && keyCode in MEDIA_KEYS) {
|
||||||
|
GearslipLog.i("keys: $keyCode before the car shows Gearslip - no media app to send it to yet")
|
||||||
|
return true
|
||||||
|
}
|
||||||
when (keyCode) {
|
when (keyCode) {
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> media.togglePlay()
|
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> media?.togglePlay()
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY -> if (!media.now.value.playing) media.togglePlay()
|
KeyEvent.KEYCODE_MEDIA_PLAY -> if (media != null && !media.now.value.playing) media.togglePlay()
|
||||||
KeyEvent.KEYCODE_MEDIA_PAUSE, KeyEvent.KEYCODE_MEDIA_STOP -> if (media.now.value.playing) media.togglePlay()
|
KeyEvent.KEYCODE_MEDIA_PAUSE, KeyEvent.KEYCODE_MEDIA_STOP -> if (media != null && media.now.value.playing) media.togglePlay()
|
||||||
KeyEvent.KEYCODE_MEDIA_NEXT -> media.next()
|
KeyEvent.KEYCODE_MEDIA_NEXT -> media?.next()
|
||||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> media.previous()
|
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> media?.previous()
|
||||||
KeyEvent.KEYCODE_SEARCH, KeyEvent.KEYCODE_VOICE_ASSIST -> voice()
|
KeyEvent.KEYCODE_SEARCH, KeyEvent.KEYCODE_VOICE_ASSIST -> voice()
|
||||||
KeyEvent.KEYCODE_CALL -> call()
|
KeyEvent.KEYCODE_CALL -> call()
|
||||||
KeyEvent.KEYCODE_ENDCALL -> endCall()
|
KeyEvent.KEYCODE_ENDCALL -> endCall()
|
||||||
|
|||||||
Reference in new issue
Block a user