From 9499150be6303e07f69dd76c90be9f80745f88a7 Mon Sep 17 00:00:00 2001 From: Walter Huf Date: Mon, 4 Sep 2023 15:17:20 -0700 Subject: [PATCH] Adds support for probing a TCP tunnel Helps for #510 --- .../hufman/androidautoidrive/AppSettings.kt | 1 + .../me/hufman/androidautoidrive/CarProber.kt | 69 +++++++++++++------ .../hufman/androidautoidrive/MainService.kt | 2 +- .../carapp/music/MusicAppMode.kt | 3 +- .../viewmodels/MusicAdvancedSettingsModel.kt | 2 + .../fragment_music_advancedsettings.xml | 16 +++++ app/src/main/res/values/strings.xml | 1 + 7 files changed, 72 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/me/hufman/androidautoidrive/AppSettings.kt b/app/src/main/java/me/hufman/androidautoidrive/AppSettings.kt index fce61ee88..3c646d977 100644 --- a/app/src/main/java/me/hufman/androidautoidrive/AppSettings.kt +++ b/app/src/main/java/me/hufman/androidautoidrive/AppSettings.kt @@ -51,6 +51,7 @@ interface AppSettings { HIDDEN_MUSIC_APPS("Hidden_Music_Apps", "com.android.bluetooth,com.clearchannel.iheartradio.connect,com.google.android.googlequicksearchbox,com.google.android.youtube,com.vanced.android.youtube", "List of music apps to hide from the app list"), FORCE_SPOTIFY_LAYOUT("Force_Spotify_Layout", "false", "Use Spotify UI Resources"), FORCE_AUDIOPLAYER_LAYOUT("Force_Audioplayer_Layout", "false", "Use legacy music screen even with Spotify resources"), + CONNECTION_PROBE_IPS("Connection_Probe_IPs", "", "Extra IP(s) to test while probe for the car"), DONATION_DAYS_COUNT("Donation_Days_Count", "0", "Number of days that the user has used the app, counting towards the donation request threshold"), DONATION_LAST_DAY("Donation_Last_Day", "2000-01-01", "The last day that the user used the app"), SPOTIFY_CONTROL_SUCCESS("Spotify_Control_Success", "false", "Whether Spotify Control api worked previously"), diff --git a/app/src/main/java/me/hufman/androidautoidrive/CarProber.kt b/app/src/main/java/me/hufman/androidautoidrive/CarProber.kt index 87db258a6..1413ab6bb 100644 --- a/app/src/main/java/me/hufman/androidautoidrive/CarProber.kt +++ b/app/src/main/java/me/hufman/androidautoidrive/CarProber.kt @@ -10,12 +10,13 @@ import io.bimmergestalt.idriveconnectkit.android.CertMangling import io.bimmergestalt.idriveconnectkit.android.IDriveConnectionStatus import io.bimmergestalt.idriveconnectkit.android.security.SecurityAccess import java.io.IOException +import java.net.InetSocketAddress import java.net.Socket /** * Tries to connect to a car */ -class CarProber(val securityAccess: SecurityAccess, val bmwCert: ByteArray?, val miniCert: ByteArray?, val j29Cert: ByteArray?): HandlerThread("CarProber") { +class CarProber(val securityAccess: SecurityAccess, val settings: AppSettings, val bmwCert: ByteArray?, val miniCert: ByteArray?, val j29Cert: ByteArray?): HandlerThread("CarProber") { companion object { val PORTS = listOf(4004, 4005, 4006, 4007, 4008) val TAG = "CarProber" @@ -25,16 +26,26 @@ class CarProber(val securityAccess: SecurityAccess, val bmwCert: ByteArray?, val var carConnection: BMWRemotingServer? = null val ProberTask = Runnable { - for (port in PORTS) { - if (probePort(port)) { - // we found a car proxy! probably - // let's try connecting to it - Log.i(TAG, "Found open socket at $port, detecting car brand") - probeCar(port) - // successful connection! - schedule(5000) + val configuredIps = settings[AppSettings.KEYS.CONNECTION_PROBE_IPS] + if (configuredIps.isNotEmpty()) { + for (configuredIp in configuredIps.split(',')) { + val parts = configuredIp.split(':') + val host = parts[0].trim() + val port = parts.getOrNull(1)?.trim()?.toIntOrNull() + if (port != null) { // the user gave us a valid port + if (probePort(host, port)) { + // let's try connecting to it + Log.i(TAG, "Found open socket at $host:$port, detecting car brand") + probeCar(host, port) + } + } else { + // scan the car ports on this host + probeHost(host) + } } } + // scan the car ports from local BCL tunnel + probeHost("127.0.0.1") schedule(2000) if (IDriveConnectionStatus.isConnected && carConnection == null) { // weird state, assert that we really have no connection @@ -58,8 +69,12 @@ class CarProber(val securityAccess: SecurityAccess, val bmwCert: ByteArray?, val schedule(1000) } + private fun isConnected(): Boolean { + return IDriveConnectionStatus.isConnected && carConnection != null + } + fun schedule(delay: Long) { - if (!IDriveConnectionStatus.isConnected || carConnection == null) { + if (!isConnected()) { handler?.removeCallbacks(ProberTask) handler?.postDelayed(ProberTask, delay) } else { @@ -68,12 +83,26 @@ class CarProber(val securityAccess: SecurityAccess, val bmwCert: ByteArray?, val } } + /** Tries connecting to a car at this host */ + private fun probeHost(host: String) { + for (port in PORTS) { + if (!isConnected() && probePort(host, port)) { + // we found a car proxy! probably + // let's try connecting to it + Log.i(TAG, "Found open socket at $port, detecting car brand") + probeCar(host, port) + } + } + } + /** * Detects whether a port is open */ - private fun probePort(port: Int): Boolean { + private fun probePort(host: String, port: Int): Boolean { try { - val socket = Socket("127.0.0.1", port) +// Log.d(TAG, "Probing $host:$port") + val socket = Socket() + socket.connect(InetSocketAddress(host, port), 100) if (socket.isConnected) { socket.close() return true @@ -88,7 +117,7 @@ class CarProber(val securityAccess: SecurityAccess, val bmwCert: ByteArray?, val /** * Attempts to detect the car brand */ - private fun probeCar(port: Int) { + private fun probeCar(host: String, port: Int) { if (!securityAccess.isConnected()) { // try again later after the security service is ready schedule(2000) @@ -106,7 +135,7 @@ class CarProber(val securityAccess: SecurityAccess, val bmwCert: ByteArray?, val else -> j29Cert } ?: continue // j29Cert is optional cdsBaseApp from MyBMW val signedCert = CertMangling.mergeBMWCert(cert, securityAccess.fetchBMWCerts(brandHint = brand)) - val conn = IDriveConnection.getEtchConnection("127.0.0.1", port, BaseBMWRemotingClient()) + val conn = IDriveConnection.getEtchConnection(host, port, BaseBMWRemotingClient()) val sas_challenge = conn.sas_certificate(signedCert) val sas_login = securityAccess.signChallenge(challenge = sas_challenge) conn.sas_login(sas_login) @@ -119,18 +148,18 @@ class CarProber(val securityAccess: SecurityAccess, val bmwCert: ByteArray?, val Log.i(TAG, "Probing detected a HMI type $hmiType") if (hmiType?.startsWith("BMW") == true) { // BMW brand - setConnectedState(port, "bmw") + setConnectedState(host, port, "bmw") success = true break } if (hmiType?.startsWith("MINI") == true) { // MINI connected - setConnectedState(port, "mini") + setConnectedState(host, port, "mini") success = true break } if (brand == "j29") { - setConnectedState(port, "j29") + setConnectedState(host, port, "j29") success = true break } @@ -156,8 +185,8 @@ class CarProber(val securityAccess: SecurityAccess, val bmwCert: ByteArray?, val } } - private fun setConnectedState(port: Int, brand: String) { - Log.i(TAG, "Successfully detected $brand connection at port $port") - IDriveConnectionStatus.setConnection(brand, "127.0.0.1", port) + private fun setConnectedState(host: String, port: Int, brand: String) { + Log.i(TAG, "Successfully detected $brand connection at port $host:$port") + IDriveConnectionStatus.setConnection(brand, host, port) } } \ No newline at end of file diff --git a/app/src/main/java/me/hufman/androidautoidrive/MainService.kt b/app/src/main/java/me/hufman/androidautoidrive/MainService.kt index 4977a6744..43c39838c 100644 --- a/app/src/main/java/me/hufman/androidautoidrive/MainService.kt +++ b/app/src/main/java/me/hufman/androidautoidrive/MainService.kt @@ -222,7 +222,7 @@ class MainService: Service() { private fun startCarProber() { if (carProberThread?.isAlive != true) { - carProberThread = CarProber(securityAccess, + carProberThread = CarProber(securityAccess, appSettings, CarAppAssetResources(applicationContext, "smartthings").getAppCertificateRaw("bmw")?.readBytes(), CarAppAssetResources(applicationContext, "smartthings").getAppCertificateRaw("mini")?.readBytes(), CarAppAssetResources(applicationContext, "cdsbaseapp").getAppCertificateRaw("")?.readBytes() diff --git a/app/src/main/java/me/hufman/androidautoidrive/carapp/music/MusicAppMode.kt b/app/src/main/java/me/hufman/androidautoidrive/carapp/music/MusicAppMode.kt index 2780a97cc..9fb6adbec 100644 --- a/app/src/main/java/me/hufman/androidautoidrive/carapp/music/MusicAppMode.kt +++ b/app/src/main/java/me/hufman/androidautoidrive/carapp/music/MusicAppMode.kt @@ -68,7 +68,8 @@ class MusicAppMode(val iDriveConnectionStatus: IDriveConnectionStatus, val capab } fun isBTConnection(): Boolean { - return TRANSPORT_PORTS.fromPort(iDriveConnectionStatus.port) == TRANSPORT_PORTS.BT + return (iDriveConnectionStatus.host == "127.0.0.1" || iDriveConnectionStatus.host == "::1") && + TRANSPORT_PORTS.fromPort(iDriveConnectionStatus.port) == TRANSPORT_PORTS.BT } fun supportsUsbAudio(): Boolean { return appSettings[AppSettings.KEYS.AUDIO_SUPPORTS_USB].toBoolean() diff --git a/app/src/main/java/me/hufman/androidautoidrive/phoneui/viewmodels/MusicAdvancedSettingsModel.kt b/app/src/main/java/me/hufman/androidautoidrive/phoneui/viewmodels/MusicAdvancedSettingsModel.kt index a2c542ac9..97463aac9 100644 --- a/app/src/main/java/me/hufman/androidautoidrive/phoneui/viewmodels/MusicAdvancedSettingsModel.kt +++ b/app/src/main/java/me/hufman/androidautoidrive/phoneui/viewmodels/MusicAdvancedSettingsModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import me.hufman.androidautoidrive.AppSettings import me.hufman.androidautoidrive.BooleanLiveSetting +import me.hufman.androidautoidrive.StringLiveSetting class MusicAdvancedSettingsModel(appContext: Context): ViewModel() { class Factory(val appContext: Context): ViewModelProvider.Factory { @@ -15,6 +16,7 @@ class MusicAdvancedSettingsModel(appContext: Context): ViewModel() { } val showAdvanced = BooleanLiveSetting(appContext, AppSettings.KEYS.SHOW_ADVANCED_SETTINGS) + val connectionProbeIps = StringLiveSetting(appContext, AppSettings.KEYS.CONNECTION_PROBE_IPS) val audioContext = BooleanLiveSetting(appContext, AppSettings.KEYS.AUDIO_FORCE_CONTEXT) val spotifyLayout = BooleanLiveSetting(appContext, AppSettings.KEYS.FORCE_SPOTIFY_LAYOUT) val audioplayerLayout = BooleanLiveSetting(appContext, AppSettings.KEYS.FORCE_AUDIOPLAYER_LAYOUT) diff --git a/app/src/main/res/layout/fragment_music_advancedsettings.xml b/app/src/main/res/layout/fragment_music_advancedsettings.xml index b7257b4ae..fdf6e585e 100644 --- a/app/src/main/res/layout/fragment_music_advancedsettings.xml +++ b/app/src/main/res/layout/fragment_music_advancedsettings.xml @@ -9,6 +9,22 @@ android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> + + + + Car Data Report: Detailed Car Capabilities: Show Advanced Settings + Try connecting to a car by TCP tunnel Voice Assistant (Beta) None found