mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 00:12:24 +01:00
Wireless start keeps knocking and starts over after a failed step
While Gearslip waits for the car to call, it also calls the car's own service every 8 seconds, for cars that publish it late. If the Bluetooth channel closes, the Wi-Fi join fails or TCP can't reach the car before its screen comes up, the start-up begins again, up to three times.
This commit is contained in:
1 parent
ef1b811920
commit
1faf418798
1 file changed
+94
-11
@@ -73,6 +73,9 @@ object WirelessLink {
|
|||||||
private const val LISTEN_MS = 120_000 // time to reconnect the phone from the car's Bluetooth menu
|
private const val LISTEN_MS = 120_000 // time to reconnect the phone from the car's Bluetooth menu
|
||||||
private const val TCP_TRIES = 12 // about a minute: a car's projection listener can start well after its Wi-Fi
|
private const val TCP_TRIES = 12 // about a minute: a car's projection listener can start well after its Wi-Fi
|
||||||
private const val WIFI_WAIT_MS = 60_000
|
private const val WIFI_WAIT_MS = 60_000
|
||||||
|
private const val KNOCK_MS = 8_000L // some cars publish their service only a while after Bluetooth connects
|
||||||
|
private const val START_RETRIES = 3
|
||||||
|
private const val RETRY_PAUSE_MS = 2_000L
|
||||||
|
|
||||||
/** Set by the activity: starts a session on the car's socket. */
|
/** Set by the activity: starts a session on the car's socket. */
|
||||||
@Volatile var onConnected: ((Socket) -> Unit)? = null
|
@Volatile var onConnected: ((Socket) -> Unit)? = null
|
||||||
@@ -91,6 +94,10 @@ object WirelessLink {
|
|||||||
@Volatile private var activeSsid: String? = null
|
@Volatile private var activeSsid: String? = null
|
||||||
/** Bumped on each Wi-Fi join and on stop, so an older run of TCP tries ends instead of racing the new one. */
|
/** Bumped on each Wi-Fi join and on stop, so an older run of TCP tries ends instead of racing the new one. */
|
||||||
@Volatile private var tcpRun = 0
|
@Volatile private var tcpRun = 0
|
||||||
|
/** The socket reached the car's projection: whatever ends the start-up now, it isn't retried. */
|
||||||
|
@Volatile private var handedOver = false
|
||||||
|
/** A failure starting over can't fix, such as a VPN in the way. */
|
||||||
|
@Volatile private var noRetry = false
|
||||||
private val writeLock = Any()
|
private val writeLock = Any()
|
||||||
|
|
||||||
/** Paired Bluetooth devices, cars or not: the driver picks theirs. Needs BLUETOOTH_CONNECT. */
|
/** Paired Bluetooth devices, cars or not: the driver picks theirs. Needs BLUETOOTH_CONNECT. */
|
||||||
@@ -109,7 +116,7 @@ object WirelessLink {
|
|||||||
worker = Thread({
|
worker = Thread({
|
||||||
// A test feature: anything unexpected ends the attempt, not the app.
|
// A test feature: anything unexpected ends the attempt, not the app.
|
||||||
try {
|
try {
|
||||||
run(app, device)
|
startUp(app, device)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.e("wireless start-up failed", e)
|
log.e("wireless start-up failed", e)
|
||||||
SessionStatus.failed("Wireless start-up failed", "${e.javaClass.simpleName}: ${e.message}")
|
SessionStatus.failed("Wireless start-up failed", "${e.javaClass.simpleName}: ${e.message}")
|
||||||
@@ -117,12 +124,56 @@ object WirelessLink {
|
|||||||
}, "gearslip-wireless").apply { isDaemon = true; start() }
|
}, "gearslip-wireless").apply { isDaemon = true; start() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A step that fails after the car's Bluetooth channel opened (the channel closing, the Wi-Fi
|
||||||
|
* join, TCP) often works the second time, so the start-up begins again, up to
|
||||||
|
* [START_RETRIES] times. Waiting for a car that never called isn't repeated: that wait is
|
||||||
|
* two minutes already.
|
||||||
|
*/
|
||||||
|
private fun startUp(context: Context, device: BluetoothDevice) {
|
||||||
|
val me = Thread.currentThread()
|
||||||
|
var tries = 0
|
||||||
|
while (true) {
|
||||||
|
handedOver = false
|
||||||
|
noRetry = false
|
||||||
|
val reached = run(context, device)
|
||||||
|
if (worker !== me || handedOver || noRetry || !reached) return
|
||||||
|
if (++tries > START_RETRIES) {
|
||||||
|
log.w("start-up failed ${tries} times; giving up")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.i("start-up failed before the car's screen came up; starting over ($tries of $START_RETRIES)")
|
||||||
|
release(context)
|
||||||
|
SessionStatus.connecting("Starting over with ${name(device)} ($tries of $START_RETRIES)")
|
||||||
|
try {
|
||||||
|
Thread.sleep(RETRY_PAUSE_MS)
|
||||||
|
} catch (_: InterruptedException) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (worker !== me) return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ends this try: the Bluetooth read loop stops, and [startUp] decides whether to go again. */
|
||||||
|
private fun endTry() {
|
||||||
|
runCatching { rfcomm?.close() }
|
||||||
|
}
|
||||||
|
|
||||||
/** Closes the Bluetooth channel, the socket and the Wi-Fi request. Safe to call any time. */
|
/** Closes the Bluetooth channel, the socket and the Wi-Fi request. Safe to call any time. */
|
||||||
fun stop(context: Context) {
|
fun stop(context: Context) {
|
||||||
tcpRun++
|
|
||||||
val starting = worker != null && SessionStatus.state.value.phase == SessionStatus.Phase.CONNECTING
|
val starting = worker != null && SessionStatus.state.value.phase == SessionStatus.Phase.CONNECTING
|
||||||
worker?.interrupt()
|
worker?.interrupt()
|
||||||
worker = null
|
worker = null
|
||||||
|
release(context)
|
||||||
|
// Stopped before the car's session began, so nothing else resets Home's card.
|
||||||
|
if (starting) {
|
||||||
|
log.i("wireless start-up stopped")
|
||||||
|
SessionStatus.disconnected()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun release(context: Context) {
|
||||||
|
tcpRun++
|
||||||
runCatching { listener?.close() }
|
runCatching { listener?.close() }
|
||||||
listener = null
|
listener = null
|
||||||
runCatching { rfcomm?.close() }
|
runCatching { rfcomm?.close() }
|
||||||
@@ -134,11 +185,6 @@ object WirelessLink {
|
|||||||
}
|
}
|
||||||
network = null
|
network = null
|
||||||
activeSsid = null
|
activeSsid = null
|
||||||
// Stopped before the car's session began, so nothing else resets Home's card.
|
|
||||||
if (starting) {
|
|
||||||
log.i("wireless start-up stopped")
|
|
||||||
SessionStatus.disconnected()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A stable id for this phone as this app sees it, standing in for the serial a real phone sends. */
|
/** A stable id for this phone as this app sees it, standing in for the serial a real phone sends. */
|
||||||
@@ -146,8 +192,9 @@ object WirelessLink {
|
|||||||
private fun deviceId(context: Context): String =
|
private fun deviceId(context: Context): String =
|
||||||
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: "gearslip"
|
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: "gearslip"
|
||||||
|
|
||||||
|
/** One try. True if the car's Bluetooth channel opened, so that a failure after it is worth retrying. */
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
private fun run(context: Context, device: BluetoothDevice) {
|
private fun run(context: Context, device: BluetoothDevice): Boolean {
|
||||||
val name = name(device)
|
val name = name(device)
|
||||||
log.i("--- wireless start-up with $name (${device.address}) ---")
|
log.i("--- wireless start-up with $name (${device.address}) ---")
|
||||||
SessionStatus.connecting("Bluetooth: reaching $name")
|
SessionStatus.connecting("Bluetooth: reaching $name")
|
||||||
@@ -163,12 +210,12 @@ object WirelessLink {
|
|||||||
"Gearslip needs the Nearby devices permission to reach $name. Allow it in the app's " +
|
"Gearslip needs the Nearby devices permission to reach $name. Allow it in the app's " +
|
||||||
"settings, then try again.",
|
"settings, then try again.",
|
||||||
)
|
)
|
||||||
return
|
return false
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
// Most factory head units publish no service of their own: they look for the phone's
|
// Most factory head units publish no service of their own: they look for the phone's
|
||||||
// and connect to it, so wait for the car instead.
|
// and connect to it, so wait for the car instead.
|
||||||
log.i("Bluetooth: $name has no Android Auto service to call (${e.message}); waiting for it to call us")
|
log.i("Bluetooth: $name has no Android Auto service to call (${e.message}); waiting for it to call us")
|
||||||
waitForCar(context, name) ?: return
|
waitForCar(context, device, name) ?: return false
|
||||||
}
|
}
|
||||||
rfcomm = socket
|
rfcomm = socket
|
||||||
log.i("Bluetooth: channel open")
|
log.i("Bluetooth: channel open")
|
||||||
@@ -261,6 +308,7 @@ object WirelessLink {
|
|||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
if (rfcomm != null) log.w("Bluetooth: channel closed (${e.message})")
|
if (rfcomm != null) log.w("Bluetooth: channel closed (${e.message})")
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -269,7 +317,7 @@ object WirelessLink {
|
|||||||
* Bluetooth, so the driver may have to reconnect the phone from the car's menu.
|
* Bluetooth, so the driver may have to reconnect the phone from the car's menu.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
private fun waitForCar(context: Context, name: String): BluetoothSocket? {
|
private fun waitForCar(context: Context, device: BluetoothDevice, name: String): BluetoothSocket? {
|
||||||
val adapter = context.getSystemService(BluetoothManager::class.java)?.adapter ?: return null
|
val adapter = context.getSystemService(BluetoothManager::class.java)?.adapter ?: return null
|
||||||
val server = try {
|
val server = try {
|
||||||
adapter.listenUsingRfcommWithServiceRecord("Android Auto", AA_UUID)
|
adapter.listenUsingRfcommWithServiceRecord("Android Auto", AA_UUID)
|
||||||
@@ -281,12 +329,40 @@ object WirelessLink {
|
|||||||
listener = server
|
listener = server
|
||||||
SessionStatus.connecting("Waiting for $name. If nothing happens, disconnect the phone in the car's Bluetooth menu and connect it again")
|
SessionStatus.connecting("Waiting for $name. If nothing happens, disconnect the phone in the car's Bluetooth menu and connect it again")
|
||||||
log.i("Bluetooth: Android Auto service published, waiting ${LISTEN_MS / 1000}s for $name")
|
log.i("Bluetooth: Android Auto service published, waiting ${LISTEN_MS / 1000}s for $name")
|
||||||
|
// Meanwhile, keep calling the car's own service: a car that publishes it late answers one
|
||||||
|
// of these instead of calling us. Whichever connects first is the channel.
|
||||||
|
val done = java.util.concurrent.atomic.AtomicBoolean(false)
|
||||||
|
val knocked = java.util.concurrent.atomic.AtomicReference<BluetoothSocket?>(null)
|
||||||
|
val knocker = Thread({
|
||||||
|
while (!done.get()) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(KNOCK_MS)
|
||||||
|
} catch (_: InterruptedException) {
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
|
if (done.get()) return@Thread
|
||||||
|
val socket = runCatching { device.createRfcommSocketToServiceRecord(AA_UUID).also { it.connect() } }
|
||||||
|
.getOrNull() ?: continue
|
||||||
|
if (done.compareAndSet(false, true)) {
|
||||||
|
log.i("Bluetooth: $name's own Android Auto service answered")
|
||||||
|
knocked.set(socket)
|
||||||
|
runCatching { server.close() }
|
||||||
|
} else {
|
||||||
|
runCatching { socket.close() }
|
||||||
|
}
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
|
}, "gearslip-wireless-knock").apply { isDaemon = true; start() }
|
||||||
return try {
|
return try {
|
||||||
server.accept(LISTEN_MS).also {
|
server.accept(LISTEN_MS).also {
|
||||||
|
if (!done.compareAndSet(false, true)) runCatching { knocked.get()?.close() }
|
||||||
val from = runCatching { it.remoteDevice.name }.getOrNull() ?: "a device"
|
val from = runCatching { it.remoteDevice.name }.getOrNull() ?: "a device"
|
||||||
log.i("Bluetooth: $from connected to the Android Auto service")
|
log.i("Bluetooth: $from connected to the Android Auto service")
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
|
knocker.join(500)
|
||||||
|
knocked.get()?.let { return it }
|
||||||
|
done.set(true)
|
||||||
if (listener == null) return null // stopped
|
if (listener == null) return null // stopped
|
||||||
log.w("Bluetooth: no car connected within ${LISTEN_MS / 1000}s (${e.message})")
|
log.w("Bluetooth: no car connected within ${LISTEN_MS / 1000}s (${e.message})")
|
||||||
SessionStatus.failed(
|
SessionStatus.failed(
|
||||||
@@ -296,6 +372,8 @@ object WirelessLink {
|
|||||||
)
|
)
|
||||||
null
|
null
|
||||||
} finally {
|
} finally {
|
||||||
|
done.set(true)
|
||||||
|
knocker.interrupt()
|
||||||
runCatching { server.close() }
|
runCatching { server.close() }
|
||||||
listener = null
|
listener = null
|
||||||
}
|
}
|
||||||
@@ -355,6 +433,7 @@ object WirelessLink {
|
|||||||
"Android didn't connect to $name's network. If a \"Connect to device\" prompt " +
|
"Android didn't connect to $name's network. If a \"Connect to device\" prompt " +
|
||||||
"appeared, it has to be accepted.",
|
"appeared, it has to be accepted.",
|
||||||
)
|
)
|
||||||
|
endTry()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Android can hold an app's traffic back on a network it has joined (battery or data
|
// Android can hold an app's traffic back on a network it has joined (battery or data
|
||||||
@@ -471,6 +550,7 @@ object WirelessLink {
|
|||||||
socket.tcpNoDelay = true
|
socket.tcpNoDelay = true
|
||||||
socket.connect(InetSocketAddress(ip, port), 5_000)
|
socket.connect(InetSocketAddress(ip, port), 5_000)
|
||||||
tcp = socket
|
tcp = socket
|
||||||
|
handedOver = true
|
||||||
log.i("TCP: connected to $ip:$port (try ${attempt + 1})")
|
log.i("TCP: connected to $ip:$port (try ${attempt + 1})")
|
||||||
val start = onConnected
|
val start = onConnected
|
||||||
if (start == null) {
|
if (start == null) {
|
||||||
@@ -498,11 +578,14 @@ object WirelessLink {
|
|||||||
"Gearslip joined $name's network, but a VPN on this phone won't let it reach the car. " +
|
"Gearslip joined $name's network, but a VPN on this phone won't let it reach the car. " +
|
||||||
"Turn the VPN off, or exclude Gearslip in its settings, then try again.",
|
"Turn the VPN off, or exclude Gearslip in its settings, then try again.",
|
||||||
)
|
)
|
||||||
|
noRetry = true
|
||||||
|
endTry()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
SessionStatus.failed(
|
SessionStatus.failed(
|
||||||
"Car's Wi-Fi joined, but no connection",
|
"Car's Wi-Fi joined, but no connection",
|
||||||
"Gearslip joined $name's network but couldn't reach Android Auto at $ip:$port.",
|
"Gearslip joined $name's network but couldn't reach Android Auto at $ip:$port.",
|
||||||
)
|
)
|
||||||
|
endTry()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user