fix(todo): turn to GPS where the fused provider stays silent (ADR 0033, D32)
A stock Android 17 emulator without Google's services left GPS off at balanced accuracy and gave no fix in five minutes, while the notification said it was watching. A Pixel's fused provider turns GPS and the network on by itself, but nothing noticed where one doesn't. After twice the wait asked for without a usable fix, the service asks GPS as well, and leaves it once the fused provider gives a usable fix; a rough one can't tell a circle, so it counts as silence. All four walks (arrive, drive past, reboot, leave) passed on that emulator on GPS alone, 22 fixes, no crash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
62f5dbb2d6
commit
5e02344699
3 changed files with 95 additions and 6 deletions
|
|
@ -58,6 +58,13 @@ object LocationDwell {
|
|||
/** Whether the phone, [moved] metres since the last fix, lies still. */
|
||||
fun still(moved: Float): Boolean = moved < STILL_METRES
|
||||
|
||||
/**
|
||||
* How long without a usable fix, [asked] being the wait asked for, until
|
||||
* the fused provider counts as silent and GPS is asked itself. Twice the
|
||||
* wait, as one fix late is no silence.
|
||||
*/
|
||||
fun silentAfter(asked: Long): Long = asked * 2
|
||||
|
||||
/**
|
||||
* How many fixes the phone has lain still, [before] this one, [moved] metres
|
||||
* from the last and [waited] milliseconds after it, [asked] being the wait
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import android.location.LocationListener
|
|||
import android.location.LocationManager
|
||||
import android.location.LocationRequest
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import androidx.core.app.ServiceCompat
|
||||
|
|
@ -26,12 +27,29 @@ import kotlin.math.abs
|
|||
* which misses a short stop altogether, and its other way of asking in the
|
||||
* background is held to the same; a foreground service isn't. Its notification
|
||||
* is the quietest there is, and the fixes come as seldom as a phone far from
|
||||
* every place, or lying still, can afford ([LocationDwell.interval]).
|
||||
* every place, or lying still, can afford ([LocationDwell.interval]). Where
|
||||
* the fused provider stays silent (on stock Android without Google's services,
|
||||
* an emulator's asked at balanced accuracy never turned GPS on), this asks GPS
|
||||
* itself until the fused provider speaks again, as a phone showing it watches
|
||||
* should watch.
|
||||
*/
|
||||
class LocationWatchService : Service(), LocationListener {
|
||||
private var requested: Long? = null
|
||||
private var last: Location? = null
|
||||
private var stillFixes = 0
|
||||
private var askingFused = false
|
||||
private var askingGps = false
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val silent = Runnable { askGps() }
|
||||
private val gps = object : LocationListener {
|
||||
override fun onLocationChanged(fix: Location) {
|
||||
take(fix)
|
||||
}
|
||||
|
||||
override fun onProviderDisabled(provider: String) = Unit
|
||||
|
||||
override fun onProviderEnabled(provider: String) = Unit
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
|
|
@ -93,23 +111,71 @@ class LocationWatchService : Service(), LocationListener {
|
|||
mainExecutor,
|
||||
this,
|
||||
)
|
||||
askingFused = true
|
||||
} else {
|
||||
manager.requestLocationUpdates(LocationManager.GPS_PROVIDER, interval, 0f, this, Looper.getMainLooper())
|
||||
askingFused = false
|
||||
}
|
||||
requested = interval
|
||||
LocationReminders.log(this, "a fix every ${interval / 1000} s")
|
||||
if (askingGps) requestGps(interval)
|
||||
expectFix()
|
||||
} catch (error: SecurityException) {
|
||||
LocationReminders.log(this, "no fixes: $error")
|
||||
}
|
||||
}
|
||||
|
||||
/** Waits for the next usable fix, and turns to GPS if none comes. */
|
||||
private fun expectFix() {
|
||||
handler.removeCallbacks(silent)
|
||||
val interval = requested ?: return
|
||||
if (askingFused) handler.postDelayed(silent, LocationDwell.silentAfter(interval))
|
||||
}
|
||||
|
||||
private fun askGps() {
|
||||
val interval = requested ?: return
|
||||
if (askingGps || !askingFused) return
|
||||
LocationReminders.log(this, "silence: asking GPS")
|
||||
askingGps = requestGps(interval)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission") // As in [ask].
|
||||
private fun requestGps(interval: Long): Boolean {
|
||||
val manager = getSystemService(LocationManager::class.java)
|
||||
return try {
|
||||
manager.removeUpdates(gps)
|
||||
manager.requestLocationUpdates(LocationManager.GPS_PROVIDER, interval, 0f, gps, Looper.getMainLooper())
|
||||
true
|
||||
} catch (error: SecurityException) {
|
||||
LocationReminders.log(this, "no GPS: $error")
|
||||
false
|
||||
} catch (error: IllegalArgumentException) {
|
||||
// No GPS on this device.
|
||||
LocationReminders.log(this, "no GPS: $error")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopGps() {
|
||||
if (!askingGps) return
|
||||
LocationReminders.log(this, "the fused provider speaks: leaving GPS")
|
||||
getSystemService(LocationManager::class.java).removeUpdates(gps)
|
||||
askingGps = false
|
||||
}
|
||||
|
||||
override fun onLocationChanged(fix: Location) {
|
||||
// A rough one doesn't count: it can't tell the circle, so GPS still can.
|
||||
if (take(fix) && askingFused) stopGps()
|
||||
}
|
||||
|
||||
/** Whether [fix] was new and usable, and so taken. */
|
||||
private fun take(fix: Location): Boolean {
|
||||
val before = last
|
||||
// What Android hands over again as it's asked anew says nothing new.
|
||||
if (before != null && fix.elapsedRealtimeNanos <= before.elapsedRealtimeNanos) return
|
||||
if (before != null && fix.elapsedRealtimeNanos <= before.elapsedRealtimeNanos) return false
|
||||
val accuracy = if (fix.hasAccuracy()) fix.accuracy else null
|
||||
LocationReminders.log(this, "fix, ${accuracy ?: "?"} m")
|
||||
if (!LocationDwell.usable(accuracy)) return
|
||||
LocationReminders.log(this, "fix, ${accuracy ?: "?"} m, ${fix.provider}")
|
||||
if (!LocationDwell.usable(accuracy)) return false
|
||||
stillFixes = if (before == null) {
|
||||
0
|
||||
} else {
|
||||
|
|
@ -123,6 +189,8 @@ class LocationWatchService : Service(), LocationListener {
|
|||
last = fix
|
||||
LocationReminders.seen(this, fix.latitude, fix.longitude)
|
||||
ask(next(fix))
|
||||
expectFix()
|
||||
return true
|
||||
}
|
||||
|
||||
// Only the phone's location being switched off or on: fixes stop and go on.
|
||||
|
|
@ -131,7 +199,10 @@ class LocationWatchService : Service(), LocationListener {
|
|||
override fun onProviderEnabled(provider: String) = Unit
|
||||
|
||||
override fun onDestroy() {
|
||||
getSystemService(LocationManager::class.java).removeUpdates(this)
|
||||
handler.removeCallbacks(silent)
|
||||
val manager = getSystemService(LocationManager::class.java)
|
||||
manager.removeUpdates(gps)
|
||||
manager.removeUpdates(this)
|
||||
running = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,12 @@ class LocationDwellTest {
|
|||
assertFalse(LocationDwell.still(moved = 50f))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `silence for twice the wait asked for turns to GPS`() {
|
||||
assertEquals(60_000L, LocationDwell.silentAfter(asked = 30_000L))
|
||||
assertEquals(600_000L, LocationDwell.silentAfter(asked = 300_000L))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a still fix after the wait asked for counts`() {
|
||||
assertEquals(3, LocationDwell.stillFixes(before = 2, moved = 10f, waited = 60_000L, asked = 60_000L))
|
||||
|
|
|
|||
Loading…
Reference in a new issue