mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
Merge pull request #86 from davideresigotti/honda-civic-screen
Honda Civic: tell the car which phone this is, so it gives up its screen
This commit is contained in:
4 files changed
+98
-18
No files matched your search
@@ -159,6 +159,9 @@ class GearslipActivity : ComponentActivity(), Projection {
|
||||
}
|
||||
}
|
||||
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)
|
||||
// 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.
|
||||
|
||||
@@ -85,6 +85,8 @@ class GearslipRunner(
|
||||
private var selectedConfigIndex: Int? = null
|
||||
private var vehicleProfile: VehicleProfile? = null
|
||||
@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
|
||||
|
||||
/** Between the car UI and the encoder when the car squashes the frame (see [screenShape]). */
|
||||
@@ -773,6 +775,13 @@ class GearslipRunner(
|
||||
bluetooth,
|
||||
open = { openChannel(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",
|
||||
)
|
||||
requestProjectedFocus()
|
||||
if (CarAudio.openSink != null) watchForWithheldScreen()
|
||||
watchForWithheldScreen()
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
private fun requestProjectedFocus(withDisplay: Boolean = false) {
|
||||
focusAsked = true
|
||||
// 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
|
||||
// keep its own screen (issue #43); 0 is UNKNOWN.
|
||||
@@ -1380,6 +1390,10 @@ class GearslipRunner(
|
||||
* 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
|
||||
* 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() {
|
||||
Thread({
|
||||
@@ -1389,11 +1403,12 @@ class GearslipRunner(
|
||||
if (open != null) {
|
||||
log.w("no screen from the car after ${WITHHELD_SCREEN_MS / 1000}s - opening the media channel and asking again")
|
||||
open()
|
||||
// Give the car the channel's open response before it hears the request again.
|
||||
Thread.sleep(1_000)
|
||||
} else {
|
||||
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)
|
||||
}, "gearslip-focus-fallback").apply { isDaemon = true }.start()
|
||||
}
|
||||
|
||||
@@ -57,11 +57,12 @@ object CarBluetooth {
|
||||
private val main = Handler(Looper.getMainLooper())
|
||||
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(
|
||||
val service: ServiceDiscovery.BluetoothService,
|
||||
val open: () -> Unit,
|
||||
val send: (messageId: Int, body: ByteArray) -> Unit,
|
||||
val answered: () -> Unit,
|
||||
)
|
||||
|
||||
@Volatile private var car: Car? = null
|
||||
@@ -104,13 +105,16 @@ object CarBluetooth {
|
||||
_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")
|
||||
private fun askForAddress(device: BluetoothDevice, carAddress: String) {
|
||||
private fun askForAddress(device: BluetoothDevice, carAddress: String, again: Boolean = false) {
|
||||
val context = appContext ?: return
|
||||
val asked = AppSettings.getString(context, KEY_ADDRESS_ASKED, "").split(",").filter { it.isNotEmpty() }
|
||||
if (hash(carAddress) in asked) return
|
||||
AppSettings.putString(context, KEY_ADDRESS_ASKED, (asked + hash(carAddress)).joinToString(","))
|
||||
if (hash(carAddress) in asked && !again) return
|
||||
AppSettings.putString(context, KEY_ADDRESS_ASKED, (asked + hash(carAddress)).distinct().joinToString(","))
|
||||
val name = runCatching { device.alias ?: device.name }.getOrNull()?.takeIf { it.isNotBlank() }
|
||||
?: context.getString(R.string.bt_address_your_car)
|
||||
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) }
|
||||
}
|
||||
|
||||
/** 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(
|
||||
service: ServiceDiscovery.BluetoothService,
|
||||
open: () -> Unit,
|
||||
send: (messageId: Int, body: ByteArray) -> Unit,
|
||||
answered: () -> Unit = {},
|
||||
) {
|
||||
car = Car(service, open, send)
|
||||
car = Car(service, open, send, answered)
|
||||
bondStarted = false
|
||||
announcing = false
|
||||
log.i("the car offers Bluetooth pairing, methods=${service.pairingMethods}")
|
||||
@@ -159,6 +167,40 @@ object CarBluetooth {
|
||||
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() {
|
||||
car = null
|
||||
pairing = false
|
||||
@@ -226,6 +268,7 @@ object CarBluetooth {
|
||||
MSG_PAIRING_RESPONSE -> {
|
||||
val fields = Wire.fields(body)
|
||||
log.i("<- PairingResponse status=${Wire.varint(fields, 1)} already_paired=${Wire.varint(fields, 2) == 1L}")
|
||||
c.answered()
|
||||
if (!pairing) return
|
||||
// Some cars start pairing themselves once they answer; give them a moment first.
|
||||
main.postDelayed({ startBond("the car answered") }, CAR_BOND_WAIT_MS)
|
||||
@@ -260,7 +303,10 @@ object CarBluetooth {
|
||||
log.i("bond state $previous -> $state")
|
||||
when {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -285,7 +331,10 @@ object CarBluetooth {
|
||||
|
||||
/** The car as a Bluetooth device, or null (and logged) when Gearslip can't reach Bluetooth. */
|
||||
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
|
||||
if (context.checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_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 }
|
||||
|
||||
/** 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
|
||||
* offered; when it offers nothing (some units leave the list empty and send keys anyway),
|
||||
@@ -87,13 +93,20 @@ object CarKeys {
|
||||
return true
|
||||
}
|
||||
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) {
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> media.togglePlay()
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY -> if (!media.now.value.playing) media.togglePlay()
|
||||
KeyEvent.KEYCODE_MEDIA_PAUSE, KeyEvent.KEYCODE_MEDIA_STOP -> if (media.now.value.playing) media.togglePlay()
|
||||
KeyEvent.KEYCODE_MEDIA_NEXT -> media.next()
|
||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> media.previous()
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> 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 != null && media.now.value.playing) media.togglePlay()
|
||||
KeyEvent.KEYCODE_MEDIA_NEXT -> media?.next()
|
||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> media?.previous()
|
||||
KeyEvent.KEYCODE_SEARCH, KeyEvent.KEYCODE_VOICE_ASSIST -> voice()
|
||||
KeyEvent.KEYCODE_CALL -> call()
|
||||
KeyEvent.KEYCODE_ENDCALL -> endCall()
|
||||
|
||||
Reference in new issue
Block a user