mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 00:12:24 +01:00
wireless: connect by itself when a car Gearslip has connected to before comes up over Bluetooth
This commit is contained in:
1 parent
6945f6755e
commit
8fbd1701b1
6 files changed
+174
No files matched your search
@@ -25,6 +25,9 @@
|
|||||||
hands over its Wi-Fi details; Gearslip then joins that network to reach the car.
|
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_CONNECT" />
|
||||||
|
<!-- Wireless auto-connect: Android tells Gearslip when a car it manages connects. -->
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE" />
|
||||||
|
<uses-feature android:name="android.software.companion_device_setup" android:required="false" />
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.NEARBY_WIFI_DEVICES"
|
android:name="android.permission.NEARBY_WIFI_DEVICES"
|
||||||
@@ -244,5 +247,15 @@
|
|||||||
<action android:name="android.telecom.CallScreeningService" />
|
<action android:name="android.telecom.CallScreeningService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<!-- Told when a car approved for wireless auto-connect comes up over Bluetooth. -->
|
||||||
|
<service
|
||||||
|
android:name=".wireless.CarPresenceService"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="android.permission.BIND_COMPANION_DEVICE_SERVICE">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.companion.CompanionDeviceService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -117,6 +117,17 @@ class GearslipActivity : ComponentActivity(), Projection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Android's one-time "Allow" for a car to connect by itself; see [WirelessAuto]. */
|
||||||
|
private var approvingCar: String? = null
|
||||||
|
private val carApproval = registerForActivityResult(
|
||||||
|
androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult(),
|
||||||
|
) { result ->
|
||||||
|
val address = approvingCar ?: return@registerForActivityResult
|
||||||
|
approvingCar = null
|
||||||
|
if (result.resultCode == RESULT_OK) app.seb3thehacker.gearslip.wireless.WirelessAuto.approved(this, address)
|
||||||
|
else GearslipLog.w("auto-connect: declined in Android's dialog")
|
||||||
|
}
|
||||||
|
|
||||||
/** Setting Gearslip as the phone's caller ID and spam app - what lets it decline a ringing call. */
|
/** Setting Gearslip as the phone's caller ID and spam app - what lets it decline a ringing call. */
|
||||||
private val callScreeningRole = registerForActivityResult(StartActivityForResult()) { result ->
|
private val callScreeningRole = registerForActivityResult(StartActivityForResult()) { result ->
|
||||||
if (result.resultCode != RESULT_OK) GearslipLog.w("calls: call screening role declined")
|
if (result.resultCode != RESULT_OK) GearslipLog.w("calls: call screening role declined")
|
||||||
@@ -138,6 +149,12 @@ class GearslipActivity : ComponentActivity(), Projection {
|
|||||||
app.seb3thehacker.gearslip.wireless.WirelessLink.onConnected = { socket ->
|
app.seb3thehacker.gearslip.wireless.WirelessLink.onConnected = { socket ->
|
||||||
runOnUiThread { startWireless(socket) }
|
runOnUiThread { startWireless(socket) }
|
||||||
}
|
}
|
||||||
|
app.seb3thehacker.gearslip.wireless.WirelessAuto.showApproval = { chooser, address ->
|
||||||
|
runOnUiThread {
|
||||||
|
approvingCar = address
|
||||||
|
carApproval.launch(androidx.activity.result.IntentSenderRequest.Builder(chooser).build())
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
||||||
@@ -216,6 +233,7 @@ class GearslipActivity : ComponentActivity(), Projection {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
app.seb3thehacker.gearslip.wireless.WirelessLink.onConnected = null
|
app.seb3thehacker.gearslip.wireless.WirelessLink.onConnected = null
|
||||||
|
app.seb3thehacker.gearslip.wireless.WirelessAuto.showApproval = null
|
||||||
runCatching { unregisterReceiver(detachReceiver) }
|
runCatching { unregisterReceiver(detachReceiver) }
|
||||||
PhoneMirror.onStartRequested = null
|
PhoneMirror.onStartRequested = null
|
||||||
PhoneMirror.onTokenReady = null
|
PhoneMirror.onTokenReady = null
|
||||||
@@ -304,6 +322,10 @@ class GearslipActivity : ComponentActivity(), Projection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleIntent(intent: Intent?) {
|
private fun handleIntent(intent: Intent?) {
|
||||||
|
if (intent?.action == app.seb3thehacker.gearslip.wireless.WirelessAuto.ACTION_CAR_NEARBY) {
|
||||||
|
connectToNearbyCar(intent.getStringExtra(app.seb3thehacker.gearslip.wireless.WirelessAuto.EXTRA_ADDRESS))
|
||||||
|
return
|
||||||
|
}
|
||||||
if (intent?.action != UsbManager.ACTION_USB_ACCESSORY_ATTACHED) return
|
if (intent?.action != UsbManager.ACTION_USB_ACCESSORY_ATTACHED) return
|
||||||
// Switched off in Settings: the system shouldn't route the car here at all, but if it
|
// Switched off in Settings: the system shouldn't route the car here at all, but if it
|
||||||
// does, leave the accessory alone.
|
// does, leave the accessory alone.
|
||||||
@@ -321,6 +343,23 @@ class GearslipActivity : ComponentActivity(), Projection {
|
|||||||
start(accessory)
|
start(accessory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A known car came up over Bluetooth: connect wirelessly, unless USB or another try is already on it. */
|
||||||
|
private fun connectToNearbyCar(address: String?) {
|
||||||
|
if (address == null || !AppSettings.gearslipEnabled(this)) return
|
||||||
|
if (descriptor != null) {
|
||||||
|
GearslipLog.i("auto-connect: the car is on USB already - staying on USB")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (runner != null || app.seb3thehacker.gearslip.wireless.WirelessLink.busy) return
|
||||||
|
if (checkSelfPermission(android.Manifest.permission.BLUETOOTH_CONNECT) != android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||||||
|
GearslipLog.w("auto-connect: needs the Nearby devices permission")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val adapter = getSystemService(android.bluetooth.BluetoothManager::class.java)?.adapter ?: return
|
||||||
|
val device = runCatching { adapter.getRemoteDevice(address) }.getOrNull() ?: return
|
||||||
|
app.seb3thehacker.gearslip.wireless.WirelessLink.start(this, device)
|
||||||
|
}
|
||||||
|
|
||||||
private fun start(accessory: UsbAccessory) {
|
private fun start(accessory: UsbAccessory) {
|
||||||
// A fresh ATTACHED intent means the system wants a new session, whatever state the old
|
// A fresh ATTACHED intent means the system wants a new session, whatever state the old
|
||||||
// one is in - closing first guarantees openAccessory() below never fights a descriptor
|
// one is in - closing first guarantees openAccessory() below never fights a descriptor
|
||||||
|
|||||||
@@ -109,6 +109,8 @@ internal fun WirelessButton() {
|
|||||||
.clickable {
|
.clickable {
|
||||||
devices = null
|
devices = null
|
||||||
WirelessLink.start(context, device)
|
WirelessLink.start(context, device)
|
||||||
|
// Next time this car connects, Gearslip does this by itself.
|
||||||
|
app.seb3thehacker.gearslip.wireless.WirelessAuto.connectedByHand(context, device)
|
||||||
}
|
}
|
||||||
.padding(vertical = 14.dp),
|
.padding(vertical = 14.dp),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package app.seb3thehacker.gearslip.wireless
|
||||||
|
|
||||||
|
import android.companion.CompanionDeviceService
|
||||||
|
|
||||||
|
/** Android calls this when a car approved in [WirelessAuto] connects over Bluetooth. */
|
||||||
|
class CarPresenceService : CompanionDeviceService() {
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun onDeviceAppeared(address: String) {
|
||||||
|
WirelessAuto.carAppeared(applicationContext, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun onDeviceDisappeared(address: String) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package app.seb3thehacker.gearslip.wireless
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.companion.AssociationRequest
|
||||||
|
import android.companion.BluetoothDeviceFilter
|
||||||
|
import android.companion.CompanionDeviceManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentSender
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import app.seb3thehacker.gearslip.GearslipLog
|
||||||
|
import app.seb3thehacker.gearslip.GearslipActivity
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts wireless Android Auto by itself when a car Gearslip has connected to before comes up
|
||||||
|
* over Bluetooth.
|
||||||
|
*
|
||||||
|
* Android only lets an app wake up and open its screen from the background for a device it
|
||||||
|
* manages as a companion, so the first wireless connection to a car also asks Android to make
|
||||||
|
* that car Gearslip's companion. Android shows its own one-time "Allow" for it; after that it
|
||||||
|
* tells [CarPresenceService] whenever the car connects, and Gearslip connects to it.
|
||||||
|
*/
|
||||||
|
object WirelessAuto {
|
||||||
|
|
||||||
|
/** Action on the activity's intent when the car came up on its own. */
|
||||||
|
const val ACTION_CAR_NEARBY = "app.seb3thehacker.gearslip.CAR_NEARBY"
|
||||||
|
const val EXTRA_ADDRESS = "address"
|
||||||
|
|
||||||
|
private const val PREFS = "gearslip_wireless_auto"
|
||||||
|
|
||||||
|
private val log = GearslipLog.tagged("AAW")
|
||||||
|
|
||||||
|
/** Set by the activity: shows Android's companion dialog, which only an activity can do. */
|
||||||
|
@Volatile var showApproval: ((IntentSender, String) -> Unit)? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever the driver connects to [device] by hand. Asks Android once per car; a car
|
||||||
|
* already approved only has its presence watched again, in case Android forgot.
|
||||||
|
*/
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun connectedByHand(context: Context, device: BluetoothDevice) {
|
||||||
|
val manager = context.getSystemService(CompanionDeviceManager::class.java) ?: return
|
||||||
|
val address = device.address
|
||||||
|
if (isCompanion(manager, address)) {
|
||||||
|
watch(context, address)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
if (prefs.getBoolean(address, false)) return
|
||||||
|
prefs.edit().putBoolean(address, true).apply()
|
||||||
|
|
||||||
|
val request = AssociationRequest.Builder()
|
||||||
|
.addDeviceFilter(BluetoothDeviceFilter.Builder().setAddress(address).build())
|
||||||
|
.setSingleDevice(true)
|
||||||
|
.build()
|
||||||
|
log.i("auto-connect: asking Android to let Gearslip watch for ${WirelessLink.name(device)}")
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
manager.associate(request, object : CompanionDeviceManager.Callback() {
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun onDeviceFound(chooser: IntentSender) {
|
||||||
|
showApproval?.invoke(chooser, address) ?: log.w("auto-connect: no screen to ask on")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(error: CharSequence?) {
|
||||||
|
log.w("auto-connect: Android wouldn't ask ($error)")
|
||||||
|
}
|
||||||
|
}, Handler(Looper.getMainLooper()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The driver allowed it in Android's dialog. */
|
||||||
|
fun approved(context: Context, address: String) {
|
||||||
|
log.i("auto-connect: allowed")
|
||||||
|
watch(context, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
private fun watch(context: Context, address: String) {
|
||||||
|
val manager = context.getSystemService(CompanionDeviceManager::class.java) ?: return
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
runCatching { manager.startObservingDevicePresence(address) }
|
||||||
|
.onFailure { log.w("auto-connect: couldn't watch for the car (${it.message})") }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun isCompanion(manager: CompanionDeviceManager, address: String): Boolean =
|
||||||
|
runCatching { manager.associations.any { it.equals(address, ignoreCase = true) } }.getOrDefault(false)
|
||||||
|
|
||||||
|
/** From [CarPresenceService]: the car is here, so bring Gearslip up to connect to it. */
|
||||||
|
fun carAppeared(context: Context, address: String) {
|
||||||
|
log.i("auto-connect: a car Gearslip knows connected over Bluetooth")
|
||||||
|
runCatching {
|
||||||
|
context.startActivity(
|
||||||
|
Intent(context, GearslipActivity::class.java)
|
||||||
|
.setAction(ACTION_CAR_NEARBY)
|
||||||
|
.putExtra(EXTRA_ADDRESS, address)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||||
|
)
|
||||||
|
}.onFailure { log.w("auto-connect: couldn't open Gearslip (${it.message})") }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,6 +82,9 @@ object WirelessLink {
|
|||||||
@Volatile private var tcp: Socket? = null
|
@Volatile private var tcp: Socket? = null
|
||||||
@Volatile private var network: ConnectivityManager.NetworkCallback? = null
|
@Volatile private var network: ConnectivityManager.NetworkCallback? = null
|
||||||
@Volatile private var worker: Thread? = null
|
@Volatile private var worker: Thread? = null
|
||||||
|
|
||||||
|
/** A start-up or a session's Bluetooth channel is under way. */
|
||||||
|
val busy: Boolean get() = worker?.isAlive == true
|
||||||
/** Set when Android drops the car's network mid-try, so the rest don't bind to a dead one. */
|
/** Set when Android drops the car's network mid-try, so the rest don't bind to a dead one. */
|
||||||
@Volatile private var lostNetwork: Network? = null
|
@Volatile private var lostNetwork: Network? = 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. */
|
||||||
|
|||||||
Reference in new issue
Block a user