LocationSource.kt
| 1 | package net.lexcom.opentracker.loc |
| 2 | |
| 3 | import kotlin.math.roundToLong |
| 4 | |
| 5 | /** |
| 6 | * Where fixes come from, and how one becomes a [Fix]. |
| 7 | * |
| 8 | * The interface exists so the service can be driven by a fake on the JVM. The |
| 9 | * device implementation is `AospLocationSource`, and it is the only file in the |
| 10 | * app that touches `android.location`. |
| 11 | * |
| 12 | * The mapping below takes primitives, never an `android.location.Location`. |
| 13 | * Unit tests run with `isReturnDefaultValues = true`, where every framework |
| 14 | * getter answers 0, false or null, so a test built on a real Location would |
| 15 | * assert nothing. Keeping the arithmetic on primitives is what makes it |
| 16 | * testable. |
| 17 | */ |
| 18 | |
| 19 | /** Widest coordinate the wire accepts. Mirrors `LAT_MAX_E7` / `LON_MAX_E7` in |
| 20 | * `crates/otproto/src/point.rs`, which rejects a point outside them. */ |
| 21 | const val LAT_MAX_E7 = 900_000_000 |
| 22 | const val LON_MAX_E7 = 1_800_000_000 |
| 23 | |
| 24 | /** |
| 25 | * Accuracy assumed when the provider makes no accuracy claim, in metres. |
| 26 | * |
| 27 | * Deliberately above [ACCURACY_CEILING_M]. A fix that does not say how good it |
| 28 | * is has not earned trust, so the policy treats it as coarse and flags it |
| 29 | * `LOW_ACCURACY`. It is not lost: the staleness bypass still accepts it once |
| 30 | * nothing better has arrived for [STALENESS_TIMEOUT_MS]. Roughly the spread of |
| 31 | * a cell-tower estimate, which is what such a fix usually is. |
| 32 | */ |
| 33 | const val ACCURACY_UNKNOWN_M = 500f |
| 34 | |
| 35 | interface LocationSource { |
| 36 | /** Begin delivering fixes. [onFix] is called on the source's callback thread. */ |
| 37 | fun start(request: Request, onFix: (Fix) -> Unit) |
| 38 | |
| 39 | /** Apply new sampling parameters, keeping the same callback. */ |
| 40 | fun update(request: Request) |
| 41 | |
| 42 | fun stop() |
| 43 | } |
| 44 | |
| 45 | /** |
| 46 | * One provider reading turned into a [Fix]. |
| 47 | * |
| 48 | * Every optional argument is null when the device did not measure it, which is |
| 49 | * what [net.lexcom.opentracker.wire.Point] encodes as the field's sentinel. |
| 50 | */ |
| 51 | fun fixFrom( |
| 52 | tsMs: Long, |
| 53 | latE7: Int, |
| 54 | lonE7: Int, |
| 55 | accuracyM: Float?, |
| 56 | speedMps: Float?, |
| 57 | altM: Float?, |
| 58 | bearingDeg: Float?, |
| 59 | fromNetwork: Boolean, |
| 60 | isMock: Boolean, |
| 61 | ): Fix = Fix( |
| 62 | tsMs = tsMs, |
| 63 | latE7 = latE7, |
| 64 | lonE7 = lonE7, |
| 65 | accM = accuracyM ?: ACCURACY_UNKNOWN_M, |
| 66 | speedMps = speedMps, |
| 67 | altM = altM, |
| 68 | bearingDeg = bearingDeg, |
| 69 | fromNetwork = fromNetwork, |
| 70 | isMock = isMock, |
| 71 | ) |
| 72 | |
| 73 | /** |
| 74 | * Degrees to the wire's 1e-7 fixed point, clamped to [maxE7]. Null when the |
| 75 | * value is not a number. |
| 76 | * |
| 77 | * Callers pass [LAT_MAX_E7] or [LON_MAX_E7]. The bound is an argument because |
| 78 | * the two axes differ by a factor of two, and a latitude clamped at the |
| 79 | * longitude bound would let a broken provider push a point past the pole. |
| 80 | * |
| 81 | * Clamping, not wrapping: a coordinate out of range means the provider is |
| 82 | * wrong, and a wrap would turn that into a plausible position somewhere else. |
| 83 | * |
| 84 | * NaN gets no coordinate at all. A mock provider can inject one, and any |
| 85 | * substitute value is a position the phone was never at. Zero would be the |
| 86 | * worst of them, because it reads as a real place in the Atlantic. The caller |
| 87 | * drops the reading instead. |
| 88 | */ |
| 89 | fun toE7(deg: Double, maxE7: Int): Int? { |
| 90 | if (deg.isNaN()) return null |
| 91 | val scaled = (deg * 1e7).roundToLong() |
| 92 | return scaled.coerceIn(-maxE7.toLong(), maxE7.toLong()).toInt() |
| 93 | } |
| 94 |