A wireless drive keeps Gearslip in the foreground

From the moment the car's Bluetooth channel opens until the drive ends, a quiet
notification keeps Gearslip in the foreground, so Android lets it join the car's
Wi-Fi with the phone locked.
This commit is contained in:
Seb3thehacker committed 2026-11-03 12:52:18 -08:00
1 parent cc78020eda
commit 07e398cf79
3 files changed
+76

No files matched your search

+7
View File
@@ -19,6 +19,8 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<!-- Keeps a wireless start going with the phone locked; see WirelessKeepAlive. -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<!--
Wireless Android Auto: Bluetooth reaches the paired car's Android Auto service, which
@@ -204,6 +206,11 @@
android:exported="false"
android:foregroundServiceType="location" />
<service
android:name=".wireless.WirelessKeepAlive"
android:exported="false"
android:foregroundServiceType="connectedDevice" />
<!--
Holds the capture session that carries a media app's audio to the car. It also claims
the microphone, so voice replies can hear the driver with the phone locked.
@@ -0,0 +1,57 @@
package app.seb3thehacker.gearslip.wireless
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.IBinder
import app.seb3thehacker.gearslip.GearslipLog
/**
* Keeps Gearslip in the foreground while a wireless car is connected, from the moment its
* Bluetooth channel opens until the drive ends. Android treats an app in the background as
* unwanted and may refuse its Wi-Fi request or kill it, which is what happens to a start-up with
* the phone locked in a pocket. It does no work of its own; the notification is the cost.
*/
class WirelessKeepAlive : Service() {
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(
NotificationChannel(CHANNEL, "Wireless car connection", NotificationManager.IMPORTANCE_LOW),
)
val notification = Notification.Builder(this, CHANNEL)
.setSmallIcon(android.R.drawable.stat_sys_data_bluetooth)
.setContentTitle("Gearslip is connected to the car wirelessly")
.setOngoing(true)
.build()
// Refused if Android thinks Gearslip may not start one now; the start-up carries on
// without it, as it did before this service existed.
runCatching {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE)
}.onFailure {
GearslipLog.w("wireless keep-alive: could not start the foreground service - ${it.message}")
stopSelf()
}
return START_NOT_STICKY
}
companion object {
private const val CHANNEL = "gearslip_wireless"
private const val NOTIFICATION_ID = 45
fun start(context: Context) {
runCatching { context.startForegroundService(Intent(context, WirelessKeepAlive::class.java)) }
.onFailure { GearslipLog.w("wireless keep-alive: start refused - ${it.message}") }
}
fun stop(context: Context) {
runCatching { context.stopService(Intent(context, WirelessKeepAlive::class.java)) }
}
}
}
@@ -132,6 +132,15 @@ object WirelessLink {
*/
private fun startUp(context: Context, device: BluetoothDevice) {
val me = Thread.currentThread()
try {
tryAndRetry(context, device, me)
} finally {
// A newer start-up has the service now; stop() already ended this one's.
if (worker === me) WirelessKeepAlive.stop(context)
}
}
private fun tryAndRetry(context: Context, device: BluetoothDevice, me: Thread) {
var tries = 0
while (true) {
handedOver = false
@@ -165,6 +174,7 @@ object WirelessLink {
worker?.interrupt()
worker = null
release(context)
WirelessKeepAlive.stop(context)
// Stopped before the car's session began, so nothing else resets Home's card.
if (starting) {
log.i("wireless start-up stopped")
@@ -219,6 +229,8 @@ object WirelessLink {
}
rfcomm = socket
log.i("Bluetooth: channel open")
// Before the Wi-Fi request: Android may refuse one from an app it sees as in the background.
WirelessKeepAlive.start(context)
SessionStatus.connecting("Bluetooth: talking to $name")
val input = DataInputStream(socket.inputStream)