mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +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
@@ -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. */
|
||||
private val callScreeningRole = registerForActivityResult(StartActivityForResult()) { result ->
|
||||
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 ->
|
||||
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)
|
||||
AppSettings.applyGearslipEnabled(this)
|
||||
// A capture that died without tidying up would leave notifications exposed to every
|
||||
@@ -216,6 +233,7 @@ class GearslipActivity : ComponentActivity(), Projection {
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
app.seb3thehacker.gearslip.wireless.WirelessLink.onConnected = null
|
||||
app.seb3thehacker.gearslip.wireless.WirelessAuto.showApproval = null
|
||||
runCatching { unregisterReceiver(detachReceiver) }
|
||||
PhoneMirror.onStartRequested = null
|
||||
PhoneMirror.onTokenReady = null
|
||||
@@ -304,6 +322,10 @@ class GearslipActivity : ComponentActivity(), Projection {
|
||||
}
|
||||
|
||||
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
|
||||
// Switched off in Settings: the system shouldn't route the car here at all, but if it
|
||||
// does, leave the accessory alone.
|
||||
@@ -321,6 +343,23 @@ class GearslipActivity : ComponentActivity(), Projection {
|
||||
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) {
|
||||
// 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
|
||||
|
||||
@@ -109,6 +109,8 @@ internal fun WirelessButton() {
|
||||
.clickable {
|
||||
devices = null
|
||||
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),
|
||||
)
|
||||
|
||||
@@ -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 network: ConnectivityManager.NetworkCallback? = 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. */
|
||||
@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. */
|
||||
|
||||
Reference in new issue
Block a user