LocationSource.kt
⎇
Raw
1package net.lexcom.opentracker.loc
2
3import 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. */
21const val LAT_MAX_E7 = 900_000_000
22const 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 */
33const val ACCURACY_UNKNOWN_M = 500f
34
35interface 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 */
51fun 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 */
89fun 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