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_FINE_LOCATION" />
<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" />
<!-- 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 Wireless Android Auto: Bluetooth reaches the paired car's Android Auto service, which
@@ -204,6 +206,11 @@
android:exported="false" android:exported="false"
android:foregroundServiceType="location" /> 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 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. 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) { private fun startUp(context: Context, device: BluetoothDevice) {
val me = Thread.currentThread() 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 var tries = 0
while (true) { while (true) {
handedOver = false handedOver = false
@@ -165,6 +174,7 @@ object WirelessLink {
worker?.interrupt() worker?.interrupt()
worker = null worker = null
release(context) release(context)
WirelessKeepAlive.stop(context)
// Stopped before the car's session began, so nothing else resets Home's card. // Stopped before the car's session began, so nothing else resets Home's card.
if (starting) { if (starting) {
log.i("wireless start-up stopped") log.i("wireless start-up stopped")
@@ -219,6 +229,8 @@ object WirelessLink {
} }
rfcomm = socket rfcomm = socket
log.i("Bluetooth: channel open") 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") SessionStatus.connecting("Bluetooth: talking to $name")
val input = DataInputStream(socket.inputStream) val input = DataInputStream(socket.inputStream)