mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 00:12:24 +01:00
wireless func added (testing)
This commit is contained in:
1 parent
9dc705c8cf
commit
39e1a5ee09
5 files changed
+421
-3
No files matched your search
@@ -19,6 +19,20 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Wireless Android Auto: Bluetooth reaches the paired car's Android Auto service, which
|
||||||
|
hands over its Wi-Fi details; Gearslip then joins that network to reach the car.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.NEARBY_WIFI_DEVICES"
|
||||||
|
android:usesPermissionFlags="neverForLocation" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
|
|
||||||
<!-- The dashboard opened from the car clock: today's agenda and current weather. -->
|
<!-- The dashboard opened from the car clock: today's agenda and current weather. -->
|
||||||
<uses-permission android:name="android.permission.READ_CALENDAR" />
|
<uses-permission android:name="android.permission.READ_CALENDAR" />
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ class GearslipActivity : ComponentActivity(), Projection {
|
|||||||
|
|
||||||
|
|
||||||
private var descriptor: ParcelFileDescriptor? = null
|
private var descriptor: ParcelFileDescriptor? = null
|
||||||
|
/** The wireless session's socket, when the car came over Wi-Fi instead of USB. */
|
||||||
|
private var wirelessSocket: java.net.Socket? = null
|
||||||
private var runner: GearslipRunner? = null
|
private var runner: GearslipRunner? = null
|
||||||
private var screenProjector: ScreenProjector? = null
|
private var screenProjector: ScreenProjector? = null
|
||||||
private var worker: Thread? = null
|
private var worker: Thread? = null
|
||||||
@@ -92,7 +94,8 @@ class GearslipActivity : ComponentActivity(), Projection {
|
|||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
if (intent?.action == UsbManager.ACTION_USB_ACCESSORY_DETACHED) {
|
if (intent?.action == UsbManager.ACTION_USB_ACCESSORY_DETACHED) {
|
||||||
GearslipLog.w("accessory detached")
|
GearslipLog.w("accessory detached")
|
||||||
closeAccessory()
|
// A wireless session has no accessory to lose; leave it running.
|
||||||
|
if (wirelessSocket == null) closeAccessory()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,6 +118,9 @@ class GearslipActivity : ComponentActivity(), Projection {
|
|||||||
GearslipLog.init(this)
|
GearslipLog.init(this)
|
||||||
GearslipLog.installCrashHandler()
|
GearslipLog.installCrashHandler()
|
||||||
GearslipLog.i("Gearslip ready")
|
GearslipLog.i("Gearslip ready")
|
||||||
|
app.seb3thehacker.gearslip.wireless.WirelessLink.onConnected = { socket ->
|
||||||
|
runOnUiThread { startWireless(socket) }
|
||||||
|
}
|
||||||
CarSettings.init(this)
|
CarSettings.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
|
||||||
@@ -127,7 +133,7 @@ class GearslipActivity : ComponentActivity(), Projection {
|
|||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
GearslipTheme {
|
GearslipTheme {
|
||||||
GearslipApp(onDisconnect = ::closeAccessory, onRequestCallScreening = ::requestCallScreeningRole)
|
GearslipApp(onDisconnect = { closeAccessory() }, onRequestCallScreening = ::requestCallScreeningRole)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,6 +167,7 @@ class GearslipActivity : ComponentActivity(), Projection {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
app.seb3thehacker.gearslip.wireless.WirelessLink.onConnected = null
|
||||||
runCatching { unregisterReceiver(detachReceiver) }
|
runCatching { unregisterReceiver(detachReceiver) }
|
||||||
PhoneMirror.onStartRequested = null
|
PhoneMirror.onStartRequested = null
|
||||||
PhoneMirror.onTokenReady = null
|
PhoneMirror.onTokenReady = null
|
||||||
@@ -296,16 +303,43 @@ class GearslipActivity : ComponentActivity(), Projection {
|
|||||||
worker = Thread { runner.run() }.apply { name = "gearslip"; start() }
|
worker = Thread { runner.run() }.apply { name = "gearslip"; start() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A wireless session: the same runner, on the socket [WirelessLink] opened to the car. */
|
||||||
|
private fun startWireless(socket: java.net.Socket) {
|
||||||
|
closeAccessory(stopWireless = false)
|
||||||
|
if (!AppSettings.gearslipEnabled(this)) {
|
||||||
|
GearslipLog.i("wireless: Gearslip is switched off - not connecting")
|
||||||
|
runCatching { socket.close() }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
GearslipLog.i("wireless: starting a session over Wi-Fi to ${socket.inetAddress?.hostAddress}:${socket.port}")
|
||||||
|
wirelessSocket = socket
|
||||||
|
val runner = GearslipRunner(
|
||||||
|
input = socket.getInputStream(),
|
||||||
|
output = socket.getOutputStream(),
|
||||||
|
identityProvider = { CertProvider.load(this) },
|
||||||
|
projection = this,
|
||||||
|
vehicleProfileFor = { info -> VehicleProfiles.find(VehicleProfiles.load(this), info) },
|
||||||
|
)
|
||||||
|
this.runner = runner
|
||||||
|
worker = Thread { runner.run() }.apply { name = "gearslip"; start() }
|
||||||
|
}
|
||||||
|
|
||||||
private fun describe(accessory: UsbAccessory) {
|
private fun describe(accessory: UsbAccessory) {
|
||||||
GearslipLog.i("accessory: manufacturer=${accessory.manufacturer} model=${accessory.model}")
|
GearslipLog.i("accessory: manufacturer=${accessory.manufacturer} model=${accessory.model}")
|
||||||
GearslipLog.i(" version=${accessory.version} description=${accessory.description}")
|
GearslipLog.i(" version=${accessory.version} description=${accessory.description}")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun closeAccessory() {
|
private fun closeAccessory() = closeAccessory(stopWireless = true)
|
||||||
|
|
||||||
|
/** Ends the session, wired or wireless. [stopWireless] also drops the car's Bluetooth and Wi-Fi. */
|
||||||
|
private fun closeAccessory(stopWireless: Boolean) {
|
||||||
runner?.stop()
|
runner?.stop()
|
||||||
SessionReport.print()
|
SessionReport.print()
|
||||||
runCatching { descriptor?.close() }
|
runCatching { descriptor?.close() }
|
||||||
descriptor = null
|
descriptor = null
|
||||||
|
runCatching { wirelessSocket?.close() }
|
||||||
|
wirelessSocket = null
|
||||||
|
if (stopWireless) app.seb3thehacker.gearslip.wireless.WirelessLink.stop(this)
|
||||||
runner = null
|
runner = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ fun HomeScreen(
|
|||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
StatusCard(status)
|
StatusCard(status)
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
if (status.phase != SessionStatus.Phase.PROJECTING) WirelessCard()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sits at the bottom, where a thumb reaches it.
|
// Sits at the bottom, where a thumb reaches it.
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package app.seb3thehacker.gearslip.ui
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Info
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import app.seb3thehacker.gearslip.wireless.WirelessLink
|
||||||
|
|
||||||
|
/** What the wireless start-up needs: Bluetooth to reach the car, nearby Wi-Fi to join its network. */
|
||||||
|
private val WIRELESS_PERMISSIONS = buildList {
|
||||||
|
add(Manifest.permission.BLUETOOTH_CONNECT)
|
||||||
|
if (Build.VERSION.SDK_INT >= 33) add(Manifest.permission.NEARBY_WIFI_DEVICES)
|
||||||
|
add(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test entry point for wireless Android Auto: pick the paired car, and Gearslip connects to it
|
||||||
|
* over Bluetooth and Wi-Fi. Automatic start comes later.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
internal fun WirelessCard() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var devices by remember { mutableStateOf<List<BluetoothDevice>?>(null) }
|
||||||
|
var denied by remember { mutableStateOf(false) }
|
||||||
|
fun granted() = WIRELESS_PERMISSIONS.all {
|
||||||
|
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
val ask = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
|
||||||
|
if (granted()) devices = WirelessLink.pairedDevices(context) else denied = true
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
SettingsRow(
|
||||||
|
"Connect wirelessly (test)",
|
||||||
|
Modifier.clickable {
|
||||||
|
denied = false
|
||||||
|
if (granted()) devices = WirelessLink.pairedDevices(context) else ask.launch(WIRELESS_PERMISSIONS)
|
||||||
|
},
|
||||||
|
subtitle = if (denied) "Gearslip needs Bluetooth and nearby Wi-Fi access for this"
|
||||||
|
else "Pick your car from the phone's paired devices",
|
||||||
|
icon = Icons.Filled.Info,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
devices?.let { list ->
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { devices = null },
|
||||||
|
title = { Text("Which car?") },
|
||||||
|
text = {
|
||||||
|
Column(Modifier.verticalScroll(rememberScrollState())) {
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
Text("No paired devices. Pair the phone with the car over Bluetooth first.")
|
||||||
|
}
|
||||||
|
list.forEach { device ->
|
||||||
|
Text(
|
||||||
|
WirelessLink.name(device),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
devices = null
|
||||||
|
WirelessLink.start(context, device)
|
||||||
|
}
|
||||||
|
.padding(vertical = 14.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = { TextButton(onClick = { devices = null }) { Text("Cancel") } },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
package app.seb3thehacker.gearslip.wireless
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothManager
|
||||||
|
import android.bluetooth.BluetoothSocket
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.MacAddress
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.NetworkRequest
|
||||||
|
import android.net.wifi.WifiNetworkSpecifier
|
||||||
|
import app.seb3thehacker.gearslip.GearslipLog
|
||||||
|
import app.seb3thehacker.gearslip.Protobuf
|
||||||
|
import app.seb3thehacker.gearslip.SessionStatus
|
||||||
|
import app.seb3thehacker.gearslip.Wire
|
||||||
|
import java.io.DataInputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Socket
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wireless Android Auto, phone side: the start-up that hands Gearslip a socket instead of a USB
|
||||||
|
* accessory. Everything after it - versions, TLS, the certificate, channels, video - is the same
|
||||||
|
* protocol [app.seb3thehacker.gearslip.GearslipRunner] already speaks over USB.
|
||||||
|
*
|
||||||
|
* 1. Bluetooth: open RFCOMM to the car's Android Auto wireless service. The car must already be
|
||||||
|
* paired with the phone, as it usually is for calls.
|
||||||
|
* 2. Over that channel, framed as [u16 length][u16 message id][protobuf]:
|
||||||
|
* <- VersionRequest -> VersionResponse
|
||||||
|
* <- StartRequest {ip, port} -> InfoRequest
|
||||||
|
* <- InfoResponse {ssid, password, bssid, security}
|
||||||
|
* 3. Join the car's Wi-Fi as a local-only network, so the phone's internet stays on mobile data.
|
||||||
|
* -> StartResponse {status} -> ConnectionStatus {status}
|
||||||
|
* 4. TCP to {ip, port} over that network, and hand the socket to [onConnected].
|
||||||
|
*
|
||||||
|
* The Bluetooth channel stays open for the whole drive: the car pings over it, and some cars end
|
||||||
|
* the session when it closes. Message layout from LIVI's head-unit side (livi-aa/src/wpp.rs) and
|
||||||
|
* aasdk's aaw protos.
|
||||||
|
*/
|
||||||
|
object WirelessLink {
|
||||||
|
|
||||||
|
private val log = GearslipLog.tagged("AAW")
|
||||||
|
|
||||||
|
/** The Android Auto wireless service every head unit publishes over Bluetooth. */
|
||||||
|
val AA_UUID: UUID = UUID.fromString("4de17a00-52cb-11e6-bdf4-0800200c9a66")
|
||||||
|
|
||||||
|
private const val MSG_START_REQUEST = 1
|
||||||
|
private const val MSG_INFO_REQUEST = 2
|
||||||
|
private const val MSG_INFO_RESPONSE = 3
|
||||||
|
private const val MSG_VERSION_REQUEST = 4
|
||||||
|
private const val MSG_VERSION_RESPONSE = 5
|
||||||
|
private const val MSG_CONNECTION_STATUS = 6
|
||||||
|
private const val MSG_START_RESPONSE = 7
|
||||||
|
private const val MSG_PING = 8
|
||||||
|
private const val MSG_PONG = 9
|
||||||
|
|
||||||
|
private const val TCP_TRIES = 5
|
||||||
|
private const val WIFI_WAIT_MS = 60_000
|
||||||
|
|
||||||
|
/** Set by the activity: starts a session on the car's socket. */
|
||||||
|
@Volatile var onConnected: ((Socket) -> Unit)? = null
|
||||||
|
|
||||||
|
@Volatile private var rfcomm: BluetoothSocket? = null
|
||||||
|
@Volatile private var tcp: Socket? = null
|
||||||
|
@Volatile private var network: ConnectivityManager.NetworkCallback? = null
|
||||||
|
@Volatile private var worker: Thread? = null
|
||||||
|
private val writeLock = Any()
|
||||||
|
|
||||||
|
/** Paired Bluetooth devices, cars or not: the driver picks theirs. Needs BLUETOOTH_CONNECT. */
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun pairedDevices(context: Context): List<BluetoothDevice> =
|
||||||
|
context.getSystemService(BluetoothManager::class.java)?.adapter?.bondedDevices
|
||||||
|
?.sortedBy { it.name ?: it.address }
|
||||||
|
.orEmpty()
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun name(device: BluetoothDevice): String = device.name ?: device.address
|
||||||
|
|
||||||
|
fun start(context: Context, device: BluetoothDevice) {
|
||||||
|
stop(context)
|
||||||
|
val app = context.applicationContext
|
||||||
|
worker = Thread({ run(app, device) }, "gearslip-wireless").apply { isDaemon = true; start() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Closes the Bluetooth channel, the socket and the Wi-Fi request. Safe to call any time. */
|
||||||
|
fun stop(context: Context) {
|
||||||
|
worker?.interrupt()
|
||||||
|
worker = null
|
||||||
|
runCatching { rfcomm?.close() }
|
||||||
|
rfcomm = null
|
||||||
|
runCatching { tcp?.close() }
|
||||||
|
tcp = null
|
||||||
|
network?.let { cb ->
|
||||||
|
runCatching { context.getSystemService(ConnectivityManager::class.java).unregisterNetworkCallback(cb) }
|
||||||
|
}
|
||||||
|
network = null
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
private fun run(context: Context, device: BluetoothDevice) {
|
||||||
|
val name = name(device)
|
||||||
|
log.i("--- wireless start-up with $name (${device.address}) ---")
|
||||||
|
SessionStatus.connecting("Bluetooth: reaching $name")
|
||||||
|
val socket = try {
|
||||||
|
context.getSystemService(BluetoothManager::class.java)?.adapter?.cancelDiscovery()
|
||||||
|
device.createRfcommSocketToServiceRecord(AA_UUID).also { it.connect() }
|
||||||
|
} catch (e: IOException) {
|
||||||
|
log.e("Bluetooth: could not open the Android Auto channel on $name", e)
|
||||||
|
SessionStatus.failed(
|
||||||
|
"Car didn't answer over Bluetooth",
|
||||||
|
"$name has no wireless Android Auto service, or it's turned off in the car. Check that " +
|
||||||
|
"the car is paired and wireless Android Auto is on.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rfcomm = socket
|
||||||
|
log.i("Bluetooth: channel open")
|
||||||
|
SessionStatus.connecting("Bluetooth: talking to $name")
|
||||||
|
|
||||||
|
val input = DataInputStream(socket.inputStream)
|
||||||
|
val output = socket.outputStream
|
||||||
|
var target: Pair<String, Int>? = null
|
||||||
|
try {
|
||||||
|
while (!Thread.currentThread().isInterrupted) {
|
||||||
|
val length = input.readUnsignedShort()
|
||||||
|
val id = input.readUnsignedShort()
|
||||||
|
val body = ByteArray(length).also { input.readFully(it) }
|
||||||
|
when (id) {
|
||||||
|
MSG_VERSION_REQUEST -> {
|
||||||
|
log.i("<- VersionRequest")
|
||||||
|
log.hex(" version", body)
|
||||||
|
// The fields' meaning isn't known; answering with the car's own numbers
|
||||||
|
// has worked on LIVI. Field 4 is required, so it goes as 0.
|
||||||
|
val f = Wire.fields(body)
|
||||||
|
val reply = Protobuf.varintField(1, Wire.varint(f, 1) ?: 1) +
|
||||||
|
Protobuf.varintField(2, Wire.varint(f, 2) ?: 0) +
|
||||||
|
Protobuf.varintField(4, 0)
|
||||||
|
send(output, MSG_VERSION_RESPONSE, reply)
|
||||||
|
log.i("-> VersionResponse")
|
||||||
|
}
|
||||||
|
MSG_START_REQUEST -> {
|
||||||
|
val f = Wire.fields(body)
|
||||||
|
val ip = f.firstOrNull { it.number == 1 && it.wireType == 2 }?.bytes?.decodeToString().orEmpty()
|
||||||
|
val port = Wire.varint(f, 2)?.toInt() ?: 0
|
||||||
|
log.i("<- StartRequest: projection at $ip:$port")
|
||||||
|
target = ip to port
|
||||||
|
send(output, MSG_INFO_REQUEST, ByteArray(0))
|
||||||
|
log.i("-> InfoRequest")
|
||||||
|
}
|
||||||
|
MSG_INFO_RESPONSE -> {
|
||||||
|
val f = Wire.fields(body)
|
||||||
|
fun text(n: Int) = f.firstOrNull { it.number == n && it.wireType == 2 }?.bytes?.decodeToString().orEmpty()
|
||||||
|
val ssid = text(1)
|
||||||
|
val password = text(2)
|
||||||
|
val bssid = text(3)
|
||||||
|
val security = Wire.varint(f, 4) ?: -1
|
||||||
|
log.i("<- InfoResponse: ssid=\"$ssid\" bssid=$bssid security=$security password=${if (password.isEmpty()) "none" else "(${password.length} chars)"}")
|
||||||
|
val (ip, port) = target ?: run {
|
||||||
|
log.w("InfoResponse before StartRequest: no address to connect to yet")
|
||||||
|
"" to 0
|
||||||
|
}
|
||||||
|
joinWifi(context, output, name, ssid, password, bssid, ip, port)
|
||||||
|
}
|
||||||
|
MSG_PING -> send(output, MSG_PONG, body)
|
||||||
|
MSG_CONNECTION_STATUS, MSG_START_RESPONSE -> {
|
||||||
|
log.i("<- message $id: status=${Wire.varint(Wire.fields(body), if (id == MSG_START_RESPONSE) 3 else 1)}")
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
log.i("<- unknown message $id (${body.size} bytes)")
|
||||||
|
log.hex(" msg", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
if (rfcomm != null) log.w("Bluetooth: channel closed (${e.message})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun send(output: OutputStream, id: Int, body: ByteArray) {
|
||||||
|
val frame = ByteArray(4 + body.size)
|
||||||
|
frame[0] = (body.size shr 8).toByte(); frame[1] = body.size.toByte()
|
||||||
|
frame[2] = (id shr 8).toByte(); frame[3] = id.toByte()
|
||||||
|
body.copyInto(frame, 4)
|
||||||
|
synchronized(writeLock) { output.write(frame); output.flush() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asks Android for the car's network. The first time, Android shows a "Connect to device"
|
||||||
|
* prompt on the phone; the driver has to accept it.
|
||||||
|
*/
|
||||||
|
private fun joinWifi(
|
||||||
|
context: Context, output: OutputStream, name: String,
|
||||||
|
ssid: String, password: String, bssid: String, ip: String, port: Int,
|
||||||
|
) {
|
||||||
|
SessionStatus.connecting("Wi-Fi: joining $name's network")
|
||||||
|
val spec = WifiNetworkSpecifier.Builder().setSsid(ssid).apply {
|
||||||
|
runCatching { MacAddress.fromString(bssid) }.getOrNull()
|
||||||
|
?.takeIf { it != MacAddress.fromString("00:00:00:00:00:00") }
|
||||||
|
?.let { setBssid(it) }
|
||||||
|
if (password.isNotEmpty()) setWpa2Passphrase(password)
|
||||||
|
}.build()
|
||||||
|
val request = NetworkRequest.Builder()
|
||||||
|
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||||
|
.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
.setNetworkSpecifier(spec)
|
||||||
|
.build()
|
||||||
|
val manager = context.getSystemService(ConnectivityManager::class.java)
|
||||||
|
val callback = object : ConnectivityManager.NetworkCallback() {
|
||||||
|
override fun onAvailable(net: Network) {
|
||||||
|
log.i("Wi-Fi: joined \"$ssid\"")
|
||||||
|
Thread({ connectTcp(output, name, net, ip, port) }, "gearslip-wireless-tcp").start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnavailable() {
|
||||||
|
log.w("Wi-Fi: couldn't join \"$ssid\" (declined, out of range, or wrong password)")
|
||||||
|
runCatching { send(output, MSG_CONNECTION_STATUS, Protobuf.varintField(1, -11)) }
|
||||||
|
SessionStatus.failed(
|
||||||
|
"Couldn't join the car's Wi-Fi",
|
||||||
|
"Android didn't connect to $name's network. If a \"Connect to device\" prompt " +
|
||||||
|
"appeared, it has to be accepted.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLost(net: Network) {
|
||||||
|
log.w("Wi-Fi: lost \"$ssid\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
network?.let { runCatching { manager.unregisterNetworkCallback(it) } }
|
||||||
|
network = callback
|
||||||
|
manager.requestNetwork(request, callback, WIFI_WAIT_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connectTcp(output: OutputStream, name: String, net: Network, ip: String, port: Int) {
|
||||||
|
SessionStatus.connecting("Wi-Fi: reaching $name")
|
||||||
|
var last: Exception? = null
|
||||||
|
repeat(TCP_TRIES) { attempt ->
|
||||||
|
try {
|
||||||
|
val socket = net.socketFactory.createSocket()
|
||||||
|
socket.tcpNoDelay = true
|
||||||
|
socket.connect(InetSocketAddress(ip, port), 5_000)
|
||||||
|
tcp = socket
|
||||||
|
log.i("TCP: connected to $ip:$port (try ${attempt + 1})")
|
||||||
|
runCatching {
|
||||||
|
send(output, MSG_START_RESPONSE, Protobuf.varintField(3, 0))
|
||||||
|
send(output, MSG_CONNECTION_STATUS, Protobuf.varintField(1, 0))
|
||||||
|
}
|
||||||
|
val start = onConnected
|
||||||
|
if (start == null) {
|
||||||
|
log.w("TCP: connected, but nothing is waiting for the socket")
|
||||||
|
socket.close()
|
||||||
|
} else {
|
||||||
|
start(socket)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} catch (e: Exception) {
|
||||||
|
last = e
|
||||||
|
log.w("TCP: try ${attempt + 1} to $ip:$port failed: ${e.message}")
|
||||||
|
Thread.sleep(1_000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.e("TCP: gave up on $ip:$port", last)
|
||||||
|
SessionStatus.failed(
|
||||||
|
"Car's Wi-Fi joined, but no connection",
|
||||||
|
"Gearslip joined $name's network but couldn't reach Android Auto at $ip:$port.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user