= EnumSet.of(EmitterType.WLAN5, EmitterType.WLAN6, EmitterType.WLAN2, EmitterType.BT, EmitterType.NR_FR2)
53 |
54 | /**
55 | * Given an emitter type, return the various characteristics we need to know
56 | * to model it.
57 | *
58 | * @return The characteristics needed to model the emitter
59 | */
60 | fun EmitterType.getRfCharacteristics(): RfCharacteristics =
61 | when (this) {
62 | EmitterType.WLAN2 -> characteristicsWlan24
63 | EmitterType.WLAN5, EmitterType.WLAN6 -> characteristicsWlan5 // small difference in frequency doesn't change range significantly
64 | EmitterType.GSM -> characteristicsGsm
65 | // maybe use separate characteristics? but they strongly depend on the used frequency...
66 | EmitterType.CDMA, EmitterType.WCDMA, EmitterType.TDSCDMA, EmitterType.LTE, EmitterType.NR -> characteristicsLte
67 | EmitterType.NR_FR2 -> characteristicsNrFr2
68 | EmitterType.BT -> characteristicsBluetooth
69 | EmitterType.INVALID -> characteristicsUnknown
70 | }
71 |
72 | // For 2.4 GHz, indoor range seems to be described as about 46 meters
73 | // with outdoor range about 90 meters. Set the minimum range to be about
74 | // 3/4 of the indoor range and the typical range somewhere between
75 | // the indoor and outdoor ranges.
76 | // However we've seem really, really long range detection in rural areas
77 | // so base the move distance on that.
78 | private val characteristicsWlan24 = RfCharacteristics(
79 | 16F * METERS,
80 | 35.0 * METERS,
81 | 300.0 * METERS, // Seen pretty long detection in very rural areas
82 | 2
83 | )
84 |
85 | private val characteristicsWlan5 = RfCharacteristics(
86 | 7F * METERS,
87 | 15.0 * METERS,
88 | 100.0 * METERS, // Seen pretty long detection in very rural areas
89 | 2
90 | )
91 |
92 | // currently not used, planned for stationary beacons if this proves feasible
93 | private val characteristicsBluetooth = RfCharacteristics(
94 | 5F * METERS,
95 | 2.0 * METERS,
96 | 100.0 * METERS, // class 1 devices can have 100 m range
97 | 2
98 | )
99 |
100 | private val characteristicsGsm = RfCharacteristics(
101 | 100F * METERS,
102 | 500.0 * METERS,
103 | 200.0 * KM, // usual max is around 35 km, but extended range can be around 200 km
104 | 1
105 | )
106 |
107 | // LTE cells are typically much smaller than GSM cells, but could also span the same huge areas.
108 | // "small cells" could actually be some 10 m in size, but assuming all cells might be
109 | // small cells would not be feasible, as it would increase requirements on accuracy and
110 | // lead to bad (overly accurate) location reports for LTE cells only seen once
111 | private val characteristicsLte = RfCharacteristics(
112 | 50F * METERS,
113 | 250.0 * METERS,
114 | 100.0 * KM, // ca 35 km for macrocells, but apparently extended range possible
115 | 1
116 | )
117 |
118 | // 5G FR2 supposedly has a range of 300 m, and up to 1 km with beam forming
119 | private val characteristicsNrFr2 = RfCharacteristics(
120 | 25F * METERS,
121 | 70.0 * METERS,
122 | 1000.0 * KM,
123 | 1
124 | )
125 |
126 | // Unknown emitter type, just throw out some values that make it unlikely that
127 | // we will ever use it (require too accurate a GPS location, etc.).
128 | private val characteristicsUnknown = RfCharacteristics(
129 | 2F * METERS,
130 | 50.0 * METERS,
131 | 100.0 * METERS,
132 | 99
133 | )
134 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | A microG/UnifiedNlp location provider backend using private on phone RF emitter database
3 |
4 |
5 | Export data
6 | Write all data to a CSV file
7 | Export is only supported on KitKat and above
8 | Export canceled
9 | Exporting…
10 | Export finished
11 | Error while exporting: %s
12 | Use Kalman filter for GPS location
13 | Recommended for devices with bad GPS
14 | Use cell tower locations
15 | When disabled, scans for cell towers will not happen
16 | Use WiFi locations
17 | When disabled, WiFi scans will not happen
18 | Active mode
19 | Enable GPS when unknown emitters are found (to fill up the database)
20 | Off
21 | Low - Enable GPS only when emitters are found, but no location could be determined at all
22 | Medium - Also enable GPS when WiFi emitters were found, but none of them could be used to determine a location
23 | High - Like above, but require better GPS accuracy (to store 5 GHz WiFis)
24 | Aggressive - Enable GPS when unknown emitters are found. Very likely to cause excessive battery drain
25 | Active mode GPS timeout
26 | Active mode: GPS on. Scanning because of %s and %d others
27 | Show nearby emitters
28 | Scans for emitters and displays result with additional information
29 | Discard bad emitters
30 | Decide which emitters should be discarded in case of inconsistent locations
31 | Default (used in Déjà Vu): only use the largest consistent group of emitters. This usually gives good accuracy, but occasionally produces wrong locations.\n\n
32 | Median: discard emitters that are unbelievably far from the median location: somewhat worse accuracy than default, but reduced chance of wrong locations.\n\n
33 | Use all emitters: sensitive to extreme outliers and often the least accurate of the three, but most likely to contain the actual location inside the accuracy circle.\n\n
34 | Often all 3 methods give (nearly) the same results, expect to see a difference only in some cases.\n\n
35 | Current setting: %s
36 |
37 | Default
38 | Median
39 | Use all
40 | Import data
41 | Select CSV created from export, Déjà Vu / Local NLP Backend database or MLS / OpenCelliD csv list
42 | Error: file format unknown
43 | Error parsing line %s
44 | Error importing database: %s
45 | How to handle emitters that already exist in local database?
46 | Replace local emitters
47 | Keep local emitters unchanged
48 | Merge emitters
49 | Updating database…
50 | Use %s
51 | Import from csv file
52 | Enter country codes (MCC) to import, comma separated. Use "x" as placeholder for digits 0–9. Leave blank to import all.
53 | Importing…
54 | Import canceled, no changes made
55 | %1$d imported, %2$d skipped
56 | Import finished
57 | Scanning…
58 | Scanning failed, maybe the backend service is disabled. Try disabling and enabling again.
59 | Nearby emitters
60 | Details for emitter %s
61 | Emitter type: %s
62 | WiFi SSID: %s
63 | Center: latitude %1$.5f, longitude %2$.5f
64 | Width east-west: %.2f m
65 | Width north-south: %.2f m
66 | Signal: %d / 5
67 | This emitter is blacklisted
68 | This emitter is not in the database
69 | Blacklist this emitter
70 | Delete emitter %s?
71 | Delete
72 | Network location provider not available
73 |
74 |
75 | Please enable Local NLP Backend again so it may ask for background location permission
76 |
77 | Scan and insert emitters into database at this location?\n
78 | latitude: %1$.5f\n
79 | longitude: %2$.5f
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/app/src/main/java/org/fitchfamily/android/dejavu/Kalman1Dim.java:
--------------------------------------------------------------------------------
1 | package org.fitchfamily.android.dejavu;
2 | /*
3 | * DejaVu - A location provider backend for microG/UnifiedNlp
4 | */
5 |
6 | /**
7 | * Created by tfitch on 8/31/17.
8 | */
9 |
10 | /*
11 | * This package inspired and largely copied from
12 | * https://github.com/villoren/KalmanLocationManager.git
13 | */
14 |
15 | /**
16 | * Copyright (c) 2014 Renato Villone
17 | *
18 | * Permission is hereby granted, free of charge, to any person obtaining a copy
19 | * of this software and associated documentation files (the "Software"), to deal
20 | * in the Software without restriction, including without limitation the rights
21 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
22 | * copies of the Software, and to permit persons to whom the Software is
23 | * furnished to do so, subject to the following conditions:
24 | *
25 | * The above copyright notice and this permission notice shall be included in all
26 | * copies or substantial portions of the Software.
27 | *
28 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
31 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
32 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
33 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
34 | * SOFTWARE.
35 | *
36 | * Changes and modifications to this code:
37 | * Copyright (C) 2017 Tod Fitch
38 | *
39 | * This program is Free Software: you can redistribute it and/or modify
40 | * it under the terms of the GNU General Public License as
41 | * published by the Free Software Foundation, either version 3 of the
42 | * License, or (at your option) any later version.
43 | *
44 | * This program is distributed in the hope that it will be useful,
45 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
46 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
47 | * GNU General Public License for more details.
48 | *
49 | * You should have received a copy of the GNU General Public License
50 | * along with this program. If not, see .
51 | */
52 |
53 |
54 | class Kalman1Dim {
55 | private final static double TIME_SECOND = 1000.0; // One second in milliseconds
56 |
57 | /**
58 | * Minimal time step.
59 | *
60 | * Assume 200 KPH (55.6 m/s) and a maximum accuracy of 3 meters, then there is no need
61 | * to update the filter any faster than 166.7 ms.
62 | *
63 | */
64 | private final static long TIME_STEP_MS = 150;
65 |
66 | /**
67 | * Last prediction time
68 | */
69 | private long mPredTime;
70 |
71 | /**
72 | * Time step. Computed from differences in prediction times.
73 | */
74 | private final double mt, mt2, mt2d2, mt3d2, mt4d4;
75 |
76 | /**
77 | * Process noise covariance. Computed from time step and process noise
78 | */
79 | private final double mQa, mQb, mQc, mQd;
80 |
81 | /**
82 | * Estimated state
83 | */
84 | private double mXa, mXb;
85 |
86 | /**
87 | * Estimated covariance
88 | */
89 | private double mPa, mPb, mPc, mPd;
90 |
91 |
92 | /**
93 | * Create a single dimension kalman filter.
94 | *
95 | * @param processNoise Standard deviation to calculate noise covariance from.
96 | * @param timeMillisec The time the filter is started.
97 | */
98 | public Kalman1Dim(double processNoise, long timeMillisec) {
99 | double mProcessNoise = processNoise;
100 |
101 | mPredTime = timeMillisec;
102 |
103 | mt = ((double)TIME_STEP_MS) / TIME_SECOND;
104 | mt2 = mt * mt;
105 | mt2d2 = mt2 / 2.0;
106 | mt3d2 = mt2 * mt / 2.0;
107 | mt4d4 = mt2 * mt2 / 4.0;
108 |
109 | // Process noise covariance
110 | double n2 = mProcessNoise * mProcessNoise;
111 | mQa = n2 * mt4d4;
112 | mQb = n2 * mt3d2;
113 | mQc = mQb;
114 | mQd = n2 * mt2;
115 |
116 | // Estimated covariance
117 | mPa = mQa;
118 | mPb = mQb;
119 | mPc = mQc;
120 | mPd = mQd;
121 | }
122 |
123 | /**
124 | * Reset the filter to the given state.
125 | *
126 | * Should be called after creation, unless position and velocity are assumed to be both zero.
127 | *
128 | * @param position
129 | * @param velocity
130 | * @param noise
131 | */
132 | public void setState(double position, double velocity, double noise) {
133 |
134 | // State vector
135 | mXa = position;
136 | mXb = velocity;
137 |
138 | // Covariance
139 | double n2 = noise * noise;
140 | mPa = n2 * mt4d4;
141 | mPb = n2 * mt3d2;
142 | mPc = mPb;
143 | mPd = n2 * mt2;
144 | }
145 |
146 | /**
147 | * Predict state.
148 | *
149 | * @param acceleration Should be 0 unless there's some sort of control input (a gas pedal, for instance).
150 | * @param timeMillisec The time the prediction is for.
151 | */
152 | public void predict(double acceleration, long timeMillisec) {
153 |
154 | long delta_t = timeMillisec - mPredTime;
155 | while (delta_t > TIME_STEP_MS) {
156 | mPredTime = mPredTime + TIME_STEP_MS;
157 |
158 | // x = F.x + G.u
159 | mXa = mXa + mXb * mt + acceleration * mt2d2;
160 | mXb = mXb + acceleration * mt;
161 |
162 | // P = F.P.F' + Q
163 | double Pdt = mPd * mt;
164 | double FPFtb = mPb + Pdt;
165 | double FPFta = mPa + mt * (mPc + FPFtb);
166 | double FPFtc = mPc + Pdt;
167 | double FPFtd = mPd;
168 |
169 | mPa = FPFta + mQa;
170 | mPb = FPFtb + mQb;
171 | mPc = FPFtc + mQc;
172 | mPd = FPFtd + mQd;
173 |
174 | delta_t = timeMillisec - mPredTime;
175 | }
176 | }
177 |
178 | /**
179 | * Update (correct) with the given measurement.
180 | *
181 | * @param position
182 | * @param noise
183 | */
184 | public void update(double position, double noise) {
185 |
186 | double r = noise * noise;
187 |
188 | // y = z - H . x
189 | double y = position - mXa;
190 |
191 | // S = H.P.H' + R
192 | double s = mPa + r;
193 | double si = 1.0 / s;
194 |
195 | // K = P.H'.S^(-1)
196 | double Ka = mPa * si;
197 | double Kb = mPc * si;
198 |
199 | // x = x + K.y
200 | mXa = mXa + Ka * y;
201 | mXb = mXb + Kb * y;
202 |
203 | // P = P - K.(H.P)
204 | double Pa = mPa - Ka * mPa;
205 | double Pb = mPb - Ka * mPb;
206 | double Pc = mPc - Kb * mPa;
207 | double Pd = mPd - Kb * mPb;
208 |
209 | mPa = Pa;
210 | mPb = Pb;
211 | mPc = Pc;
212 | mPd = Pd;
213 |
214 | }
215 |
216 | /**
217 | * @return Estimated position.
218 | */
219 | public double getPosition() {
220 | return mXa;
221 | }
222 |
223 | /**
224 | * @return Estimated velocity.
225 | */
226 | public double getVelocity() {
227 | return mXb;
228 | }
229 |
230 | /**
231 | * @return Accuracy
232 | */
233 | public double getAccuracy() {
234 | return Math.sqrt(mPd / mt2);
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/app/src/main/java/org/fitchfamily/android/dejavu/Cache.kt:
--------------------------------------------------------------------------------
1 | package org.fitchfamily.android.dejavu
2 |
3 | /*
4 | * Local NLP Backend / DejaVu - A location provider backend for microG/UnifiedNlp
5 | *
6 | * Copyright (C) 2017 Tod Fitch
7 | * Copyright (C) 2022 Helium314
8 | *
9 | * This program is Free Software: you can redistribute it and/or modify
10 | * it under the terms of the GNU General Public License as
11 | * published by the Free Software Foundation, either version 3 of the
12 | * License, or (at your option) any later version.
13 | *
14 | * This program is distributed in the hope that it will be useful,
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | * GNU General Public License for more details.
18 | *
19 | * You should have received a copy of the GNU General Public License
20 | * along with this program. If not, see .
21 | */
22 |
23 | import android.content.Context
24 | import android.util.Log
25 |
26 | /**
27 | * Created by tfitch on 10/4/17.
28 | * modified by helium314 in 2022
29 | */
30 | /**
31 | * All access to the database, except for import/export, is done through this cache:
32 | *
33 | * When a RF emitter is seen a get() call is made to the cache. If we have a cache hit
34 | * the information is directly returned. If we have a cache miss we create a new record
35 | * and populate it with either default information.
36 | * Emitters are not loaded from database when using get(), they need to be loaded first
37 | * using loadIds(), which channels all the emitters to load into a single db query
38 | *
39 | * Periodically we are asked to sync any new or changed RF emitter information to the
40 | * database. When that occurs we group all the changes in one database transaction for
41 | * speed.
42 | *
43 | * If an emitter has not been used for a while we will remove it from the cache (only
44 | * immediately after a sync() operation so the record will be clean). If the cache grows
45 | * too large we will clear it to conserve RAM (this should never happen). Again the
46 | * clear operation will only occur after a sync() so any dirty records will be flushed
47 | * to the database.
48 | *
49 | * Operations on the cache are thread safe. However the underlying RF emitter objects
50 | * that are returned by the cache are not thread safe. So all work on them should be
51 | * performed either in a single thread or with synchronization.
52 | */
53 | internal class Cache(context: Context?) {
54 | /**
55 | * Map (since they all must have different identifications) of
56 | * all the emitters we are working with.
57 | */
58 | private val workingSet = hashMapOf()
59 | private var db: Database? = Database.instance ?: Database(context)
60 |
61 | /**
62 | * Release all resources associated with the cache. If the cache is
63 | * dirty, then it is synced to the on flash database.
64 | */
65 | fun close() {
66 | synchronized(this) {
67 | sync()
68 | this.clear()
69 | db?.close()
70 | db = null
71 | }
72 | }
73 |
74 | /**
75 | * Queries the cache with the given RfIdentification.
76 | *
77 | * If the emitter does not exist in the cache, a new
78 | * a new "unknown" entry is created.
79 | * It is NOT fetched from database in this case.
80 | * This should be done be calling loadIds before cache.get,
81 | * because fetching emitters one by one is slower than
82 | * getting all at once. And cache.get is ALWAYS called
83 | * in a loop over many ids
84 | *
85 | * @param id
86 | * @return the emitter
87 | */
88 | operator fun get(id: RfIdentification): RfEmitter {
89 | val key = id.uniqueId
90 | return workingSet[key]?.apply { resetAge() } ?: run {
91 | val result = RfEmitter(id)
92 | synchronized(this) { workingSet[key] = result }
93 | result
94 | }
95 | }
96 |
97 | /** Simply gets the emitter if it's cached */
98 | fun simpleGet(id: RfIdentification): RfEmitter? = workingSet[id.uniqueId]
99 |
100 | /**
101 | * Loads the given RfIdentifications from database
102 | *
103 | * This is a performance improvement over loading emitters on get(),
104 | * as all emitters are loaded in a single db query.
105 | * Emitters not loaded from db are still added to the working set. This is done
106 | * because usually [get] is called on each id after loading, and adding a new
107 | * id requires synchronized, which my be a bit slow.
108 | */
109 | fun loadIds(ids: Collection) {
110 | val idsToLoad = ids.filterNot { workingSet.containsKey(it.uniqueId) }
111 | if (DEBUG) Log.d(TAG, "loadIds() - Fetching ${idsToLoad.size} ids not in working set from db.")
112 | if (idsToLoad.isEmpty()) return
113 | synchronized(this) {
114 | val emitters = db?.getEmitters(idsToLoad) ?: return
115 | emitters.forEach { workingSet[it.uniqueId] = it }
116 | idsToLoad.forEach {
117 | if (!workingSet.containsKey(it.uniqueId))
118 | workingSet[it.uniqueId] = RfEmitter(it)
119 | }
120 | }
121 | }
122 |
123 | /**
124 | * Remove all entries from the cache.
125 | */
126 | fun clear() {
127 | synchronized(this) {
128 | workingSet.clear()
129 | if (DEBUG) Log.d(TAG, "clear() - entry")
130 | }
131 | }
132 |
133 | /**
134 | * Updates the database entry for any new or changed emitters.
135 | * Once the database has been synchronized, cull infrequently used
136 | * entries. If our cache is still to big after culling, we reset
137 | * our cache.
138 | */
139 | fun sync() {
140 | if (db == null) return
141 |
142 | synchronized(this) {
143 | // Scan all of our emitters to see
144 | // 1. If any have dirty data to sync to the flash database
145 | // 2. If any have been unused long enough to remove from cache
146 | val agedEmitters = mutableListOf()
147 | val emittersInNeedOfSync = mutableListOf()
148 | workingSet.values.forEach {
149 | if (it.age >= MAX_AGE)
150 | agedEmitters.add(it.rfIdentification)
151 | it.incrementAge()
152 | if (it.syncNeeded())
153 | emittersInNeedOfSync.add(it)
154 | }
155 |
156 | if (emittersInNeedOfSync.isNotEmpty()) db?.let { db ->
157 | if (DEBUG) Log.d(TAG, "sync() - syncing ${emittersInNeedOfSync.size} emitters with db")
158 | db.beginTransaction()
159 | emittersInNeedOfSync.forEach {
160 | it.sync(db)
161 | }
162 | db.endTransaction()
163 | }
164 |
165 | // Remove aged out items from cache
166 | agedEmitters.forEach {
167 | workingSet.remove(it.uniqueId)
168 | if (DEBUG) Log.d(TAG, "sync('${it.uniqueId}') - Aged out, removed from cache.")
169 | }
170 |
171 | // clear cache is we have really a lot of emitters cached
172 | if (workingSet.size > MAX_WORKING_SET_SIZE) {
173 | if (DEBUG) Log.d(TAG, "sync() - Working set larger than $MAX_WORKING_SET_SIZE, clearing working set.")
174 | workingSet.clear()
175 | }
176 | }
177 | }
178 |
179 | companion object {
180 | private const val MAX_WORKING_SET_SIZE = 500
181 | private const val MAX_AGE = 30
182 | private val DEBUG = BuildConfig.DEBUG
183 | private const val TAG = "LocalNLP Cache"
184 | }
185 |
186 | }
187 |
--------------------------------------------------------------------------------
/app/src/main/java/org/fitchfamily/android/dejavu/Kalman.java:
--------------------------------------------------------------------------------
1 | package org.fitchfamily.android.dejavu;
2 |
3 | /*
4 | * DejaVu - A location provider backend for microG/UnifiedNlp
5 | *
6 | */
7 |
8 | /**
9 | * Created by tfitch on 8/31/17.
10 | */
11 |
12 | /*
13 | * This package inspired by https://github.com/villoren/KalmanLocationManager.git
14 | */
15 |
16 |
17 | /**
18 | * Copyright (c) 2014 Renato Villone
19 | *
20 | * Permission is hereby granted, free of charge, to any person obtaining a copy
21 | * of this software and associated documentation files (the "Software"), to deal
22 | * in the Software without restriction, including without limitation the rights
23 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
24 | * copies of the Software, and to permit persons to whom the Software is
25 | * furnished to do so, subject to the following conditions:
26 | *
27 | * The above copyright notice and this permission notice shall be included in all
28 | * copies or substantial portions of the Software.
29 | *
30 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
31 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
32 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
33 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
34 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
35 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
36 | * SOFTWARE.
37 | *
38 | * Changes and modifications to the original file:
39 | *
40 | * Copyright (C) 2017 Tod Fitch
41 | *
42 | * This program is Free Software: you can redistribute it and/or modify
43 | * it under the terms of the GNU General Public License as
44 | * published by the Free Software Foundation, either version 3 of the
45 | * License, or (at your option) any later version.
46 | *
47 | * This program is distributed in the hope that it will be useful,
48 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
49 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
50 | * GNU General Public License for more details.
51 | *
52 | * You should have received a copy of the GNU General Public License
53 | * along with this program. If not, see .
54 | */
55 |
56 | import static org.fitchfamily.android.dejavu.UtilKt.*;
57 |
58 | import android.location.Location;
59 | import android.os.Bundle;
60 | import android.os.SystemClock;
61 |
62 | /**
63 | * A two dimensional Kalman filter for estimating actual position from multiple
64 | * measurements. We cheat and use two one dimensional Kalman filters which works
65 | * because our two dimensions are orthogonal.
66 | */
67 | class Kalman {
68 | private static final double ALTITUDE_NOISE = 10.0;
69 |
70 | private static final float MOVING_THRESHOLD = 0.7f; // meters/sec (2.5 kph ~= 0.7 m/s)
71 | private static final float MIN_ACCURACY = 3.0f; // Meters
72 |
73 | /**
74 | * Three 1-dimension trackers, since the dimensions are independent and can avoid using matrices.
75 | */
76 | private final Kalman1Dim mLatTracker;
77 | private final Kalman1Dim mLonTracker;
78 | private Kalman1Dim mAltTracker;
79 |
80 | /**
81 | * Most recently computed mBearing. Only updated if we are moving.
82 | */
83 | private float mBearing = 0.0f;
84 |
85 | /**
86 | * Time of last update. Used to determine how stale our position is.
87 | */
88 | long timeOfUpdate;
89 |
90 | /**
91 | * Number of samples filter has used.
92 | */
93 | private long samples;
94 |
95 | /**
96 | *
97 | * @param location
98 | */
99 |
100 | public Kalman(Location location, double coordinateNoise) {
101 | final double accuracy = location.getAccuracy();
102 | final double coordinateNoiseDegrees = coordinateNoise * METER_TO_DEG;
103 | double position, noise;
104 | long timeMs = location.getTime();
105 |
106 | // Latitude
107 | position = location.getLatitude();
108 | noise = accuracy * METER_TO_DEG;
109 | mLatTracker = new Kalman1Dim(coordinateNoiseDegrees, timeMs);
110 | mLatTracker.setState(position, 0.0, noise);
111 |
112 | // Longitude
113 | position = location.getLongitude();
114 | noise = accuracy * Math.cos(Math.toRadians(location.getLatitude())) * METER_TO_DEG;
115 | mLonTracker = new Kalman1Dim(coordinateNoiseDegrees, timeMs);
116 | mLonTracker.setState(position, 0.0, noise);
117 |
118 | // Altitude
119 | if (location.hasAltitude()) {
120 | position = location.getAltitude();
121 | noise = accuracy;
122 | mAltTracker = new Kalman1Dim(ALTITUDE_NOISE, timeMs);
123 | mAltTracker.setState(position, 0.0, noise);
124 | }
125 | timeOfUpdate = timeMs;
126 | samples = 1;
127 | }
128 |
129 | public synchronized void update(Location location) {
130 | if (location == null)
131 | return;
132 |
133 | // Reusable
134 | final double accuracy = location.getAccuracy();
135 | double position, noise;
136 | long timeMs = location.getTime();
137 |
138 | predict(timeMs);
139 | timeOfUpdate = timeMs;
140 | samples++;
141 |
142 | // Latitude
143 | position = location.getLatitude();
144 | noise = accuracy * METER_TO_DEG;
145 | mLatTracker.update(position, noise);
146 |
147 | // Longitude
148 | position = location.getLongitude();
149 | noise = accuracy * Math.cos(Math.toRadians(location.getLatitude())) * METER_TO_DEG ;
150 | mLonTracker.update(position, noise);
151 |
152 | // Altitude
153 | if (location.hasAltitude()) {
154 | position = location.getAltitude();
155 | noise = accuracy;
156 | if (mAltTracker == null) {
157 | mAltTracker = new Kalman1Dim(ALTITUDE_NOISE, timeMs);
158 | mAltTracker.setState(position, 0.0, noise);
159 | } else {
160 | mAltTracker.update(position, noise);
161 | }
162 | }
163 | }
164 |
165 | private synchronized void predict(long timeMs) {
166 | mLatTracker.predict(0.0, timeMs);
167 | mLonTracker.predict(0.0, timeMs);
168 | if (mAltTracker != null)
169 | mAltTracker.predict(0.0, timeMs);
170 | }
171 |
172 | // Allow others to override our sample count. They may want to have us report only the
173 | // most recent samples.
174 | public void setSamples(long s) {
175 | samples = s;
176 | }
177 |
178 | public long getSamples() {
179 | return samples;
180 | }
181 |
182 | public synchronized Location getLocation() {
183 | long timeMs = System.currentTimeMillis();
184 | final Location location = new Location(LOCATION_PROVIDER);
185 |
186 | predict(timeMs);
187 | location.setTime(timeMs);
188 | location.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
189 | location.setLatitude(mLatTracker.getPosition());
190 | location.setLongitude(mLonTracker.getPosition());
191 | if (mAltTracker != null)
192 | location.setAltitude(mAltTracker.getPosition());
193 |
194 | float accuracy = (float) (mLatTracker.getAccuracy() * DEG_TO_METER);
195 | if (accuracy < MIN_ACCURACY)
196 | accuracy = MIN_ACCURACY;
197 | location.setAccuracy(accuracy);
198 |
199 | // Derive speed from degrees/ms in lat and lon
200 | double latVeolocity = mLatTracker.getVelocity() * DEG_TO_METER;
201 | double lonVeolocity = mLonTracker.getVelocity() * DEG_TO_METER *
202 | Math.cos(Math.toRadians(location.getLatitude()));
203 | float speed = (float) Math.sqrt((latVeolocity*latVeolocity)+(lonVeolocity*lonVeolocity));
204 | location.setSpeed(speed);
205 |
206 | // Compute bearing only if we are moving. Report old bearing
207 | // if we are below our threshold for moving.
208 | if (speed > MOVING_THRESHOLD) {
209 | mBearing = (float) Math.toDegrees(Math.atan2(latVeolocity, lonVeolocity));
210 | }
211 | location.setBearing(mBearing);
212 |
213 | Bundle extras = new Bundle();
214 | extras.putLong("AVERAGED_OF", samples);
215 | location.setExtras(extras);
216 |
217 | return location;
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en)
5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6 |
7 | ## [Unreleased]
8 | ### Added
9 | - Not applicable
10 |
11 | ### Changed
12 | - Not applicable
13 |
14 | ### Removed
15 | - Not applicable
16 |
17 | ## [1.2.15] - 2025-07-25
18 | ### Changed
19 | - More check against 0 coordinates
20 | - Update description texts
21 | - Upgrade dependencies
22 |
23 | ## [1.2.14] - 2025-01-22
24 | ### Changed
25 | - Added settings to launcher to allow data export without microG support
26 | - Upgrade dependencies
27 |
28 | ## [1.2.13] - 2024-12-22
29 | ### Changed
30 | - Fix wrong check breaking imports
31 | - Extend blacklist
32 | - Upgrade dependencies
33 |
34 | ## [1.2.12] - 2024-04-22
35 | ### Changed
36 | - Extend blacklist
37 | - Avoid crashes due to invalid emitter type
38 | - Upgrade dependencies
39 |
40 | ## [1.2.11] - 2023-08-20
41 | ### Changed
42 | - Import MLS / OpenCelliD lists without header
43 |
44 | ## [1.2.10] - 2023-05-22
45 | ### Changed
46 | - Extended blacklist (thanks to Sorunome)
47 | - Avoid searching nearby WiFis if GPS accuracy isn't good enough
48 |
49 | ## [1.2.9] - 2023-05-03
50 | ### Added
51 | - Handle geo uris: allows adding emitters as if a GPS location was received at the indicated location
52 |
53 | ### Changed
54 | - Improved blacklisting of unbelievably large emitters
55 |
56 | ## [1.2.8] - 2023-04-27
57 | ### Changed
58 | - Fix potential import / export issue
59 |
60 | ## [1.2.7] - 2023-04-25
61 | ### Changed
62 | - Crash fix
63 | - Small UI changes when viewing nearby emitters and emitter details
64 |
65 | ## [1.2.6] - 2023-04-19
66 | ### Changed
67 | - Notification text for active mode now contains name of emitter that triggered the scan
68 | - Keep screen on during import / export operations
69 |
70 | ## [1.2.5] - 2023-02-10
71 | ### Changed
72 | - Fix MLS import not working without MCC filter
73 | - Support placeholder for simplified MCC filtering
74 | - Fix bugs when importing files
75 | - Clarify that OpenCelliD files can be used too, as the format is same as MLS
76 | - Switch from Light theme to DayNight theme
77 |
78 | ## [1.2.4] - 2023-01-30
79 | ### Changed
80 | - Fix not (properly) asking for background location, resulting in no location permissions being asked on Android 11+
81 | - Update microG NLP API and other dependencies
82 |
83 | ## [1.2.3] - 2022-12-16
84 | ### Changed
85 | - Extend blacklist
86 | - Allow more aggressive active mode settings: fill the database better, but may increase battery use
87 |
88 | ## [1.2.2] - 2022-12-11
89 | ### Changed
90 | - Different application id for debug builds
91 | - Fix mobile emitters not being stored on some devices
92 | - Improve storing/updating emitters, especially when using active mode
93 | - Extend blacklist
94 |
95 | ## [1.2.2.beta.1] - 2022-10-11
96 | ### Added
97 | - Support for 5G and TDSCDMA cells
98 |
99 | ### Changed
100 | - Fix crashes
101 | - Upgrade to API 33
102 |
103 | ## [1.2.1] - 2022-10-05
104 | ### Added
105 | - Manually blacklist emitter when showing nearby emitters.
106 | - Active mode: enable GPS when emitters are found, but none has a known location (disabled by default).
107 |
108 | ### Changed
109 | - Update blacklist.
110 |
111 | ## [1.2.0] - 2022-09-25
112 | ### Changed
113 | - Update blacklist.
114 |
115 | ## [1.2.0-beta.4] - 2022-09-13
116 | ### Changed
117 | - Fix database import from content URI. Now import should work on all devices.
118 |
119 | ## [1.2.0-beta.3] - 2022-09-08
120 | ### Changed
121 | - fix crash when showing nearby emitters
122 | - slightly less ugly buttons when showing nearby emitters
123 |
124 | ## [1.2.0-beta.2] - 2022-09-07
125 | ### Added
126 | - Progress bars for import and export
127 |
128 | ### Changed
129 | - fix MLS import for LTE cells
130 | - fix import of files exported with Local NLP Backend
131 | - faster import
132 | - reworked database code
133 | - upgrade dependencies
134 | - prepare for API upgrade (will remove deprecated getNeighboringCellInfo function, which may be used by some old devices)
135 |
136 | ## [1.2.0-beta] - 2022-09-07
137 | ### Added
138 | - UI with capabilities to import/export emitters, show nearby emitters, select whether to use mobile cells and/or WiFi emitters, enable Kalman position filtering, and decide how to decide which emitters should be discarded in case of conflicting position reports.
139 | - Blacklist emitters with suspiciously high radius, as they may actually be mobile hotspots.
140 | - Don't use outdated WiFi scan results if scan is not successful. This helps especially against WiFi throttling introduced in Android 9.
141 | - Consider signal strength when estimating accuracy.
142 |
143 | ### Changed
144 | - New app and package names.
145 | - New icon (modified from: https://thenounproject.com/icon/38241).
146 | - Some small bug fixes.
147 | - Update and actually use the WiFi blacklist.
148 | - Faster, but less exact distance calculations. For the used distances up to 100 km, the differences are negligible.
149 | - Ignore cell emitters with invalid LAC.
150 | - Try waiting until a WiFi scan is finished before reporting location. This avoids reporting a low accuracy mobile cell location followed by more precise WiFi-based location.
151 | - Consider that LTE and 3G cells are usually smaller than GSM cells.
152 | - Don't update emitters when GPS location and emitter timestamps differ by more than 10 seconds. This reduces issues with aggressive power saving functionality by Android.
153 | - Adjusted how position and accuracy are determined.
154 |
155 | ### Removed
156 | - Emitters will stay in the database forever, instead of being removed if not found in expected locations. In original *Déjà Vu*, many WiFi emitters are removed when they cannot be found for a while, e.g. because of thick walls. Having useless entries in the database is better than removing actually existing WiFis. Additionally this change reduces database writes and background processing considerably.
157 | - Emitters will not be moved if they are found far away from their known location, as this mostly leads to bad location reports in connection with mobile hotspots. Instead they are blacklisted.
158 |
159 | ## [1.1.12] - 2019-08-12
160 |
161 | ### Changed
162 | - Update gradle build environment.
163 | - Add debug logging for detection of 5G WiFi/WLAN networds.
164 | - Add some Czech, Austrian and Dutch transport WLANs to ignore list.
165 |
166 | ## [1.1.11] - 2019-04-21
167 | ### Added
168 | - Add Esperanto and Polish translations
169 |
170 | ### Changed
171 | - Update gradle build environment
172 | - Revise list of WLAN/WiFi SSIDs to ignore
173 |
174 | ## [1.1.10] - 2018-12-18
175 | ### Added
176 | - Ignore WLANs on trains and buses of transit agencies in southwest Sweden. Thanks to lbschenkel
177 | - Ignore Austrian train WLANs. Thanks to akallabeth
178 |
179 | ### Changed
180 | - Update Gradle build environment
181 | - Revise checks for locations near lat/lon of 0,0
182 |
183 | ## [1.1.9] - 2018-09-06
184 | ### Added
185 | - Chinese translation (thanks to @Crystal-RainSlide)
186 | - Protect against external GPS giving locations near 0.0/0.0
187 |
188 | ## [1.1.8] - 2018-06-21
189 | ### Added
190 | - Initial support for 5 GHz WLAN RF characteristics being different than 2.4 GHz WLAN. Note: 5GHz WLAN not tested on a real device.
191 |
192 | ### Changed
193 | - Fix timing related crash on start up/shut down
194 | - Revisions to better support external GPS with faster report rates.
195 | - Revise database to allow same identifier on multiple emitter types.
196 | - Updated build tools and target API version
197 |
198 | ## [1.1.7] - 2018-06-18
199 | ### Changed
200 | - Fix crash on empty set of seen emitters.
201 | - Fix some Lint identified items.
202 |
203 | ## [1.1.6] - 2018-06-17
204 | ### Added
205 | - Add Ukrainian translation
206 |
207 | ### Changed
208 | - Build specification to reduce size of released application.
209 | - Update build environment
210 |
211 | ## [1.1.5] - 2018-03-19
212 | ### Added
213 | - Russian Translation. Thanks to @bboa
214 |
215 | ## [1.1.4] - 2018-03-12
216 | ### Added
217 | - German Translation. Thanks to @levush
218 |
219 | ## [1.1.3] - 2018-02-27
220 |
221 | ### Changed
222 | - Protect against accessing null object.
223 |
224 | ## [1.1.2] - 2018-02-11
225 |
226 | ### Changed
227 | - Fix typo in Polish strings. Thanks to @verdulo
228 |
229 | ## [1.1.1] - 2018-01-30
230 | ### Changed
231 | - Refactor/clean up logic flow and position computation.
232 |
233 | ## [1.1.0] - 2018-01-25
234 | ### Changed
235 | - Refactor RF emitter and database logic to allow for non-square coverage bounding boxes. Should result in more precise coverage mapping and thus better location estimation. Database file schema changed.
236 |
237 | ## [1.0.8] - 2018-01-12
238 | ### Added
239 | - Polish Translation. Thanks to @verdulo
240 |
241 | ## [1.0.7] - 2018.01.05
242 | ### Changed
243 | - Avoid crash on start up if database is not available when first RF emitter is processed.
244 |
245 | ## [1.0.6] - 2017-12-28
246 | ### Added
247 | - French translation. Thanks to @Massedil.
248 |
249 | ## [1.0.5] - 2017-12-24
250 | ### Added
251 | - Partial support for CDMA and WCDMA towers when using getAllCellInfo() API.
252 |
253 | ### Changed
254 | - Check for unknown values in fields in the cell IDs returned by getAllCellInfo();
255 |
256 | ## [1.0.4] - 2017-12-18
257 | ### Changed
258 | - Add more checks for permissions not granted to avoid locking up phone.
259 |
260 | ## [1.0.3]
261 | ### Changed
262 | - Correct blacklist logic
263 |
264 | ## [1.0.2]
265 | ### Changed
266 | - Correct versionCode and versionName in gradle.build
267 |
268 | ## [1.0.1]
269 | ### Changed
270 | - Corrected package ID in manifest
271 |
272 | ## [1.0.0]
273 | ### Added
274 | - Initial Release
275 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
90 | ' "$PWD" ) || exit
91 |
92 | # Use the maximum available, or set MAX_FD != -1 to use that value.
93 | MAX_FD=maximum
94 |
95 | warn () {
96 | echo "$*"
97 | } >&2
98 |
99 | die () {
100 | echo
101 | echo "$*"
102 | echo
103 | exit 1
104 | } >&2
105 |
106 | # OS specific support (must be 'true' or 'false').
107 | cygwin=false
108 | msys=false
109 | darwin=false
110 | nonstop=false
111 | case "$( uname )" in #(
112 | CYGWIN* ) cygwin=true ;; #(
113 | Darwin* ) darwin=true ;; #(
114 | MSYS* | MINGW* ) msys=true ;; #(
115 | NONSTOP* ) nonstop=true ;;
116 | esac
117 |
118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
119 |
120 |
121 | # Determine the Java command to use to start the JVM.
122 | if [ -n "$JAVA_HOME" ] ; then
123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
124 | # IBM's JDK on AIX uses strange locations for the executables
125 | JAVACMD=$JAVA_HOME/jre/sh/java
126 | else
127 | JAVACMD=$JAVA_HOME/bin/java
128 | fi
129 | if [ ! -x "$JAVACMD" ] ; then
130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
131 |
132 | Please set the JAVA_HOME variable in your environment to match the
133 | location of your Java installation."
134 | fi
135 | else
136 | JAVACMD=java
137 | if ! command -v java >/dev/null 2>&1
138 | then
139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
140 |
141 | Please set the JAVA_HOME variable in your environment to match the
142 | location of your Java installation."
143 | fi
144 | fi
145 |
146 | # Increase the maximum file descriptors if we can.
147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
148 | case $MAX_FD in #(
149 | max*)
150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
151 | # shellcheck disable=SC2039,SC3045
152 | MAX_FD=$( ulimit -H -n ) ||
153 | warn "Could not query maximum file descriptor limit"
154 | esac
155 | case $MAX_FD in #(
156 | '' | soft) :;; #(
157 | *)
158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
159 | # shellcheck disable=SC2039,SC3045
160 | ulimit -n "$MAX_FD" ||
161 | warn "Could not set maximum file descriptor limit to $MAX_FD"
162 | esac
163 | fi
164 |
165 | # Collect all arguments for the java command, stacking in reverse order:
166 | # * args from the command line
167 | # * the main class name
168 | # * -classpath
169 | # * -D...appname settings
170 | # * --module-path (only if needed)
171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
172 |
173 | # For Cygwin or MSYS, switch paths to Windows format before running java
174 | if "$cygwin" || "$msys" ; then
175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
177 |
178 | JAVACMD=$( cygpath --unix "$JAVACMD" )
179 |
180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
181 | for arg do
182 | if
183 | case $arg in #(
184 | -*) false ;; # don't mess with options #(
185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
186 | [ -e "$t" ] ;; #(
187 | *) false ;;
188 | esac
189 | then
190 | arg=$( cygpath --path --ignore --mixed "$arg" )
191 | fi
192 | # Roll the args list around exactly as many times as the number of
193 | # args, so each arg winds up back in the position where it started, but
194 | # possibly modified.
195 | #
196 | # NB: a `for` loop captures its iteration list before it begins, so
197 | # changing the positional parameters here affects neither the number of
198 | # iterations, nor the values presented in `arg`.
199 | shift # remove old arg
200 | set -- "$@" "$arg" # push replacement arg
201 | done
202 | fi
203 |
204 |
205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
207 |
208 | # Collect all arguments for the java command:
209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
210 | # and any embedded shellness will be escaped.
211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
212 | # treated as '${Hostname}' itself on the command line.
213 |
214 | set -- \
215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
216 | -classpath "$CLASSPATH" \
217 | org.gradle.wrapper.GradleWrapperMain \
218 | "$@"
219 |
220 | # Stop when "xargs" is not available.
221 | if ! command -v xargs >/dev/null 2>&1
222 | then
223 | die "xargs is not available"
224 | fi
225 |
226 | # Use "xargs" to parse quoted args.
227 | #
228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
229 | #
230 | # In Bash we could simply go:
231 | #
232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
233 | # set -- "${ARGS[@]}" "$@"
234 | #
235 | # but POSIX shell has neither arrays nor command substitution, so instead we
236 | # post-process each arg (as a line of input to sed) to backslash-escape any
237 | # character that might be a shell metacharacter, then use eval to reverse
238 | # that process (while maintaining the separation between arguments), and wrap
239 | # the whole thing up as a single "set" statement.
240 | #
241 | # This will of course break if any of these variables contains a newline or
242 | # an unmatched quote.
243 | #
244 |
245 | eval "set -- $(
246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
247 | xargs -n1 |
248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
249 | tr '\n' ' '
250 | )" '"$@"'
251 |
252 | exec "$JAVACMD" "$@"
253 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Note that microG has stopped supporting UnifiedNlp backends with 0.2.28.
2 |
3 | If you still want to use this backend (or others), you need to use older microG versions. This can only be recommended if you use microG __for location only__.
4 |
5 | Personally I use [0.2.10](https://github.com/microg/GmsCore/releases/tag/v0.2.10.19420), as with later versions location backends stop providing locations after some time.
6 |
7 | Local NLP Backend - A Déjà Vu Fork
8 | ==================================
9 | This is a backend for [UnifiedNlp](https://github.com/microg/android_packages_apps_UnifiedNlp) that uses locally acquired WLAN/WiFi AP and mobile/cellular tower data to resolve user location. Collectively, “WLAN/WiFi and mobile/cellular” signals will be called “RF emitters” below.
10 |
11 | Conceptually, this backend consists of two parts sharing a common database. One part passively monitors the GPS. If the GPS has acquired a position and has good position accuracy, the coverage maps for RF emitters detected by the phone are created and saved.
12 |
13 | The other part is the actual location provider which uses the database to estimate the location when the GPS is not available.
14 | This backend uses no network data. All data acquired by the phone stays on the phone.
15 |
16 | [
](https://f-droid.org/packages/helium314.localbackend/)
17 | [
](https://apt.izzysoft.de/packages/helium314.localbackend)
18 | [
](https://github.com/Helium314/Local-NLP-Backend/releases/latest)
19 |
20 | Note that F-Droid and GitHub releases use a different signing key. You cannot switch from one to the other without uninstalling Local NLP Backend first. However, you can always install the debug version (only available on GitHub) in addition to the normal version.
21 |
22 | See the [changelog](CHANGELOG.md) starting at 1.2.0-beta for a full list of changes starting from the last version of *Déjà Vu*.
23 |
24 | How to use
25 | ==========
26 | Local NLP Backend can be used like *Déjà Vu*: just enable the backend and let it build up the database by frequently having GPS enabled, e.g. using a map app.
27 | If you have a *Déjà Vu* database (you'll need root privileges to extract it), it can be imported in Local NLP Backend. Further import options are databases exported by Local NLP Backend, and cell csv files from MLS or OpenCelliD.
28 | Note that the local database needs to be filled either using GPS or by importing data, before Local NLP Backend can provide locations!
29 |
30 | In order to speed up building the database, Local NLP Backend has an optional active mode that enables GPS when there is no known emitter nearby (low setting) or when any unknown emitter is found (aggressive setting).
31 | If you have a bad GPS signal at a location, you can share a location using geo uri to Local NLP Backend, e.g. using OSMAnd share -> "geo:" or StreetComplete "open location in another app". This will cause Local NLP Backend to act as if a GPS location was received at the indicated location, and allows you to manually build a database even without GPS.
32 |
33 | On [some Android versions](https://developer.android.com/guide/topics/connectivity/wifi-scan#wifi-scan-throttling), the ability to perform WiFi scans is severely limited. Local NLP Backend does not have control over this, and is limited by the specified background app limit.
34 |
35 | Potential improvements not yet implemented
36 | ======================
37 | Local NLP Backend works mostly fine as it is, but there are some areas where it could be improved:
38 | * characteristics for the various different emitters are roughly estimated from various sources on the internet. Fine tuning of the values might improve location accuracy, especially when also considering frequency effects on range.
39 | * make use of bluetooth emitters. Bluetooth has low range and thus a good potential of giving accurate locations, but is difficult to use properly as many bluetooth emitters are mobile.
40 | * make use of [WiFi-RTT](https://developer.android.com/guide/topics/connectivity/wifi-rtt) for distance estimation. This has the potential to vastly improve precision, but works only on a small number of devices.
41 | * determination of position from found emitters is just working "good enough", but not great. A different approach might yield better results.
42 | * country code filtering in cell import currently requires lookup of the codes from some other source, this could be improved to allow for simply entering chosen countries instead.
43 |
44 | Requirements on phone
45 | =====================
46 | This is a plug-in for [microG](https://microg.org/) (UnifiedNlp or GmsCore).
47 |
48 | Setup on phone
49 | ==============
50 | In the NLP Controller app (interface for microG UnifiedNlp) select the "Local NLP Backend". If using GmsCore, you can find it in microG Settings -> Location modules. Tap on backend name for the configuration UI.
51 |
52 | When enabled, microG will request you grant location permissions to this backend. This is required so that the backend can monitor mobile/cell tower data and so that it can monitor the positions reported by the GPS.
53 |
54 | Note: The microG configuration check requires a location from a location backend to indicate that it is setup properly. However, this backend will not return a location until it has mapped at least one mobile cell tower or two WLAN/WiFi access points, or data was imported. So it may be necessary to run an app that uses the GPS for a while before this backend will report information to microG. You may wish to also install a different backend to verify microG setup quickly.
55 |
56 | Collecting RF Emitter Data
57 | ======================
58 | To conserve power the collection process by default does not actually turn on the GPS. If some other app turns on the GPS, for example a map or navigation app, then the backend will monitor the location and collect RF emitter data.
59 | Alternatively you can enable active mode in the settings available via microG backend configuration.
60 |
61 | What is stored in the database
62 | ------------------------------
63 | For each RF emitter detected an estimate of its coverage area (center and extents) is saved.
64 |
65 | For WLAN/WiFi APs the SSID is also saved for debug purposes. Analysis of the SSIDs detected by the phone can help identify name patterns used on mobile APs. The backend removes records from the database if the RF emitter has a SSID that is associated with WLAN/WiFi APs that are often mobile (e.g. "Joes iPhone").
66 |
67 | Clearing the database
68 | ---------------------
69 | This software does not have a clear or reset database function built in but you can use settings->Storage->Internal shared storage->Apps->Local NLP Backend->Clear Data to remove the current database.
70 |
71 | Permissions Required
72 | ====================
73 | |Permission|Use|
74 | |:----------|:---|
75 | ACCESS_COARSE_LOCATION|Allows backend to determine which cell towers your phone detects.
76 | ACCESS_FINE_LOCATION|Allows backend to determine which WiFis your phone detect and monitor position reports from the GPS.
77 | ACCESS_BACKGROUND_LOCATION|Necessary on Android 10 and higher, as the backend only runs in foreground when using active mode.
78 | CHANGE_WIFI_STATE|Allows backend to scan for nearby WiFis.
79 | ACCESS_WIFI_STATE|Allows backend to access WiFi scan results.
80 | FOREGROUND_SERVICE|Needed so GPS can be used in active mode.
81 |
82 | Some permissions may not be necessary, this heavily depends on the [Android version](https://developer.android.com/guide/topics/connectivity/wifi-scan).
83 |
84 | Changes
85 | =======
86 | [Revision history is kept in a separate change log.](CHANGELOG.md)
87 |
88 | Credits
89 | =======
90 | The Kalman filter used in this backend is based on [work by @villoren](https://github.com/villoren/KalmanLocationManager.git).
91 |
92 | Most of this project is adjusted from [Déjà Vu](https://github.com/n76/DejaVu)
93 |
94 | License
95 | =======
96 |
97 | Most of this project is licensed by GNU GPL. The Kalman filter code retains its original MIT license.
98 |
99 | Icon
100 | ----
101 | The icon for this project is derived from:
102 |
103 | [Geolocation icon](https://commons.wikimedia.org/wiki/File:Geolocation_-_The_Noun_Project.svg) released under [CC0 license](https://creativecommons.org/publicdomain/zero/1.0/deed.en).
104 |
105 | GNU General Public License
106 | --------------------------
107 | Copyright (C) 2017-18 Tod Fitch
108 | Copyright (C) 2022-23 Helium314
109 |
110 | This program is Free Software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
111 |
112 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
113 |
114 | You should have received a copy of the GNU General Public License
115 |
116 | MIT License
117 | -----------
118 | Permission is hereby granted, free of charge, to any person obtaining a copy
119 | of this software and associated documentation files (the "Software"), to deal
120 | in the Software without restriction, including without limitation the rights
121 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
122 | copies of the Software, and to permit persons to whom the Software is
123 | furnished to do so, subject to the following conditions:
124 |
125 | The above copyright notice and this permission notice shall be included in all
126 | copies or substantial portions of the Software.
127 |
128 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
129 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
130 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
131 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
132 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
133 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
134 | SOFTWARE.
135 |
--------------------------------------------------------------------------------
/app/src/main/java/org/fitchfamily/android/dejavu/GpsMonitor.kt:
--------------------------------------------------------------------------------
1 | package org.fitchfamily.android.dejavu
2 |
3 | import android.app.NotificationChannel
4 | import android.app.NotificationManager
5 | import android.app.Service
6 | import android.content.BroadcastReceiver
7 | import android.content.Context
8 | import android.content.Intent
9 | import android.content.IntentFilter
10 | import android.location.Location
11 | import android.location.LocationListener
12 | import android.location.LocationManager
13 | import android.os.*
14 | import android.util.Log
15 | import androidx.core.app.NotificationCompat
16 | import androidx.core.app.NotificationManagerCompat
17 | import androidx.localbroadcastmanager.content.LocalBroadcastManager
18 | import kotlinx.coroutines.*
19 | import org.fitchfamily.android.dejavu.BackendService.Companion.instanceGpsLocationUpdated
20 |
21 | /*
22 | * Local NLP Backend / DejaVu - A location provider backend for microG/UnifiedNlp
23 | *
24 | * Copyright (C) 2017 Tod Fitch
25 | * Copyright (C) 2023 Helium314
26 | *
27 | * This program is Free Software: you can redistribute it and/or modify
28 | * it under the terms of the GNU General Public License as
29 | * published by the Free Software Foundation, either version 3 of the
30 | * License, or (at your option) any later version.
31 | *
32 | * This program is distributed in the hope that it will be useful,
33 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
34 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
35 | * GNU General Public License for more details.
36 | *
37 | * You should have received a copy of the GNU General Public License
38 | * along with this program. If not, see .
39 | */
40 | /**
41 | * Created by tfitch on 8/31/17.
42 | */
43 | /**
44 | * A passive GPS monitor. We don't want to turn on the GPS as the backend
45 | * runs continuously and we would quickly drain the battery. But if some
46 | * other app turns on the GPS we want to listen in on its reports. The GPS
47 | * reports are used as a primary (trusted) source of position that we can
48 | * use to map the coverage of the RF emitters we detect.
49 | */
50 | class GpsMonitor : Service(), LocationListener {
51 | private val locationManager: LocationManager by lazy { applicationContext.getSystemService(LOCATION_SERVICE) as LocationManager }
52 | private val gpsLocationManager: LocationManager by lazy { applicationContext.getSystemService(LOCATION_SERVICE) as LocationManager }
53 | private var monitoring = false
54 | private var gpsEnabled = false
55 | override fun onBind(intent: Intent): IBinder {
56 | Log.d(TAG, "onBind() entry.")
57 | return Binder()
58 | }
59 |
60 | // for active mode
61 | private val scope: CoroutineScope by lazy { CoroutineScope(Job() + Dispatchers.IO) }
62 | private var gpsRunning: Job? = null
63 | private var targetAccuracy = 0.0f
64 | private val intentFilter = IntentFilter(ACTIVE_MODE_ACTION)
65 | private val broadcastReceiver = object : BroadcastReceiver() {
66 | override fun onReceive(context: Context?, intent: Intent?) {
67 | if (DEBUG) Log.d(TAG, "onReceive() - received intent")
68 | val time = intent?.extras?.getLong(ACTIVE_MODE_TIME) ?: return
69 | val accuracy = intent.extras?.getFloat(ACTIVE_MODE_ACCURACY) ?: return
70 | val text = intent.extras?.getString(ACTIVE_MODE_TEXT) ?: return
71 | getGpsPosition(time, accuracy, text)
72 | }
73 | }
74 |
75 | // without notification, gps will only run in if app is in foreground (i.e. in settings)
76 | private fun getNotification(text: String) =
77 | NotificationCompat.Builder(this, CHANNEL_ID)
78 | .setSmallIcon(R.drawable.ic_notification)
79 | .setPriority(NotificationCompat.PRIORITY_LOW) // only relevant for API < 28
80 | .setStyle(NotificationCompat.BigTextStyle().bigText(text)) // necessary for line breaks
81 | .build()
82 |
83 | override fun onCreate() {
84 | Log.d(TAG, "onCreate()")
85 | // before we can use the notification, we need a channel on Oreo and above
86 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
87 | val notificationManager = NotificationManagerCompat.from(this)
88 | val channel = NotificationChannel(CHANNEL_ID , getString(R.string.pref_active_mode_title), NotificationManager.IMPORTANCE_LOW)
89 | notificationManager.createNotificationChannel(channel)
90 | }
91 |
92 | monitoring = try {
93 | locationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, GPS_SAMPLE_TIME, GPS_SAMPLE_DISTANCE, this)
94 | true
95 | } catch (ex: SecurityException) {
96 | Log.w(TAG, "onCreate() failed: ", ex)
97 | false
98 | }
99 | gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
100 | LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, intentFilter)
101 | }
102 |
103 | override fun onDestroy() {
104 | super.onDestroy()
105 | Log.d(TAG, "onDestroy()")
106 | if (monitoring) {
107 | locationManager.removeUpdates(this)
108 | if (gpsRunning?.isActive == true)
109 | stopGps()
110 | monitoring = false
111 | }
112 | LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver)
113 | }
114 |
115 | /**
116 | * The passive provider we are monitoring will give positions from all
117 | * providers on the phone (including ourselves) we ignore all providers other
118 | * than the GPS. The GPS reports we pass on to our main backend service for
119 | * it to use in mapping RF emitter coverage.
120 | *
121 | * At least one Bluetooth GPS unit seems to return locations near 0.0,0.0
122 | * until it has a good lock. This can result in our believing the local
123 | * emitters are located on "null island" which then leads to other problems.
124 | * So protect ourselves and ignore any GPS readings close to 0.0,0.0 as there
125 | * is no land in that area and thus no possibility of mobile or WLAN emitters.
126 | *
127 | * @param location A position report from a location provider
128 | */
129 | override fun onLocationChanged(location: Location) {
130 | if (location.provider == LocationManager.GPS_PROVIDER) {
131 | if (gpsRunning?.isActive == true && location.accuracy <= targetAccuracy) {
132 | if (DEBUG) Log.d(TAG, "onLocationChanged() - target accuracy achieved (${location.accuracy} m), stopping GPS")
133 | stopGps()
134 | }
135 | instanceGpsLocationUpdated(location)
136 | }
137 | }
138 |
139 | @Deprecated("Deprecated in Java")
140 | override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {
141 | Log.d(TAG, "onStatusChanged() - provider $provider, status $status")
142 | }
143 |
144 | override fun onProviderEnabled(provider: String) {
145 | Log.d(TAG, "onProviderEnabled() - $provider")
146 | gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
147 | }
148 |
149 | override fun onProviderDisabled(provider: String) {
150 | Log.d(TAG, "onProviderDisabled() - $provider")
151 | // todo: apparently this is sometimes seconds after GPS was disabled, anything that can be done here?
152 | gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
153 | }
154 |
155 | /**
156 | * Try getting GPS location for a while. Will be stopped after a location with the target accuracy
157 | * is received or the timeout is over.
158 | */
159 | private fun getGpsPosition(timeout: Long, accuracy: Float, notificationText: String) {
160 | if (!gpsEnabled || gpsRunning?.isActive == true) {
161 | if (DEBUG) Log.d(TAG, "getGpsPosition() - not starting GPS. GPS provider enabled: $gpsEnabled, GPS running: ${gpsRunning?.isActive}")
162 | return
163 | }
164 | if (DEBUG) Log.d(TAG, "getGpsPosition() - trying to start for $timeout ms with accuracy target $accuracy m")
165 | try {
166 | val notification = getNotification(notificationText)
167 | startForeground(NOTIFICATION_ID, notification)
168 | notification.`when` = System.currentTimeMillis()
169 | gpsLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, GPS_SAMPLE_TIME, GPS_SAMPLE_DISTANCE, this)
170 | gpsRunning = scope.launch(Dispatchers.IO) { gpsTimeout(timeout) }
171 | targetAccuracy = accuracy
172 | } catch (ex: SecurityException) {
173 | Log.w(TAG, "getGpsPosition() - starting GPS failed", ex)
174 | }
175 | }
176 |
177 | /**
178 | * Wait for [timeout] ms and then stop GPS updates. Via [gpsRunning] this also serves as
179 | * indicator whether active GPS is on.
180 | * This is NOT delay([timeout]), because delay does not advance when the system is sleeping,
181 | * while elapsedRealtime does.
182 | */
183 | private suspend fun gpsTimeout(timeout: Long) {
184 | val t = SystemClock.elapsedRealtime()
185 | while (SystemClock.elapsedRealtime() < t + timeout) {
186 | delay(200)
187 | }
188 | if (DEBUG) Log.d(TAG, "gpsTimeout() - stopping GPS")
189 | stopGps()
190 | }
191 |
192 | private fun stopGps() {
193 | gpsLocationManager.removeUpdates(this)
194 | gpsRunning?.cancel()
195 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
196 | stopForeground(STOP_FOREGROUND_REMOVE)
197 | else
198 | stopForeground(true)
199 | }
200 |
201 | companion object {
202 | private const val TAG = "LocalNLP GpsMonitor"
203 | private val DEBUG = BuildConfig.DEBUG
204 | private const val GPS_SAMPLE_TIME = 0L
205 | private const val GPS_SAMPLE_DISTANCE = 0f
206 | }
207 | }
208 |
209 | const val ACTIVE_MODE_TIME = "time"
210 | const val ACTIVE_MODE_ACCURACY = "accuracy"
211 | const val ACTIVE_MODE_ACTION = "start_gps"
212 | const val ACTIVE_MODE_TEXT = "text"
213 | private const val NOTIFICATION_ID = 76593265 // does it matter?
214 | private const val CHANNEL_ID = "gps_active"
215 |
--------------------------------------------------------------------------------
/app/src/main/java/org/fitchfamily/android/dejavu/Util.kt:
--------------------------------------------------------------------------------
1 | package org.fitchfamily.android.dejavu
2 |
3 | /*
4 | * Local NLP Backend / DejaVu - A location provider backend for microG/UnifiedNlp
5 | *
6 | * Copyright (C) 2017 Tod Fitch
7 | * Copyright (C) 2022 Helium314
8 | *
9 | * This program is Free Software: you can redistribute it and/or modify
10 | * it under the terms of the GNU General Public License as
11 | * published by the Free Software Foundation, either version 3 of the
12 | * License, or (at your option) any later version.
13 | *
14 | * This program is distributed in the hope that it will be useful,
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | * GNU General Public License for more details.
18 | *
19 | * You should have received a copy of the GNU General Public License
20 | * along with this program. If not, see .
21 | */
22 |
23 | import android.location.Location
24 | import android.net.wifi.ScanResult
25 | import android.os.Bundle
26 | import android.util.Log
27 | import kotlin.math.*
28 |
29 | private val DEBUG = BuildConfig.DEBUG
30 | private const val TAG = "LocalNLP Util"
31 |
32 | // DEG_TO_METER is only approximate, but an error of 1% is acceptable
33 | // for latitude it depends on latitude, from ~110500 (equator) ~111700 (poles)
34 | // for longitude at equator it's ~111300
35 | const val DEG_TO_METER = 111225.0
36 | const val METER_TO_DEG = 1.0 / DEG_TO_METER
37 | const val MIN_COS = 0.01 // for things that are dividing by the cosine
38 |
39 | private const val NULL_ISLAND_DISTANCE = 1000f
40 | private const val NULL_ISLAND_DISTANCE_DEG = NULL_ISLAND_DISTANCE * METER_TO_DEG
41 |
42 | // Define range of received signal strength to be used for all emitter types.
43 | // Basically use the same range of values for LTE and WiFi as GSM defaults to.
44 | const val MAXIMUM_ASU = 31
45 | const val MINIMUM_ASU = 1
46 |
47 | // KPH -> Meters/millisec (KPH * 1000) / (60*60*1000) -> KPH/3600
48 | // const val EXPECTED_SPEED = 120.0f / 3600 // 120KPH (74 MPH)
49 | const val LOCATION_PROVIDER = "LocalNLP"
50 | private const val MINIMUM_BELIEVABLE_ACCURACY = 15.0F
51 |
52 | // much faster than location.distanceTo(otherLocation)
53 | // and less than 0.1% difference the small (< 1°) distances we're interested in
54 | fun approximateDistance(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
55 | val distLat = (lat1 - lat2)
56 | val meanLatRadians = Math.toRadians((lat1 + lat2) / 2)
57 | val distLon = (lon1 - lon2) * approxCos(meanLatRadians)
58 | return sqrt(distLat * distLat + distLon * distLon) * DEG_TO_METER
59 | }
60 |
61 | // for the short distances we use, approximate cosine is sufficient, and 5-10 times faster
62 | private fun approxCos(radians: Double): Double {
63 | val rSquared = radians * radians // multiplying often is MUCH faster than calling radians.pow (because integers get converted to double)
64 | return 1.0 - rSquared / 2 + rSquared * rSquared / 24 - rSquared * rSquared * rSquared / 720
65 | }
66 |
67 | /**
68 | * Check if location too close to null island to be real
69 | *
70 | * @param loc The location to be checked
71 | * @return boolean True if away from lat,lon of 0,0
72 | */
73 | fun notNullIsland(loc: Location): Boolean = !isNullIsland(loc.latitude, loc.longitude)
74 |
75 | fun isNullIsland(lat: Double, lon: Double): Boolean {
76 | if (lat == 0.0 && lon == 0.0) return true
77 | return abs(lat) < NULL_ISLAND_DISTANCE_DEG
78 | && abs(lon) < NULL_ISLAND_DISTANCE_DEG
79 | // only do relatively slow distance calculation if really necessary
80 | && approximateDistance(lat, lon, 0.0, 0.0) < NULL_ISLAND_DISTANCE
81 | }
82 |
83 | // wifiManager.is6GHzBandSupported might be called to check whether it can be WLAN6
84 | // but wifiManager.is5GHzBandSupported incorrectly returns no on some devices, so can we trust
85 | // it to be correct for 6 GHz?
86 | // anyway, there might be a better way of determining WiFi type
87 | fun ScanResult.getWifiType(): EmitterType =
88 | when {
89 | frequency < 3000 -> EmitterType.WLAN2 // 2401 - 2495 MHz
90 | // 5945 can be WLAN5 and WLAN6, simply don't bother and assume WLAN5 for now
91 | frequency <= 5945 -> EmitterType.WLAN5 // 5030 - 5990 MHz, but at 5945 WLAN6 starts
92 | frequency > 6000 -> EmitterType.WLAN6 // 5945 - 7125
93 | frequency % 10 == 5 -> EmitterType.WLAN6 // in the overlapping range, WLAN6 frequencies end with 5
94 | else -> EmitterType.WLAN5
95 | }
96 |
97 | /**
98 | *
99 | * The collector service attempts to detect and not report moved/moving emitters.
100 | * But it (and thus our database) can't be perfect. This routine looks at all the
101 | * emitters and returns the largest subset (group) that are within a reasonable
102 | * distance of one another.
103 | *
104 | * The hope is that a single moved/moving emitters that is seen now but whose
105 | * location was detected miles away can be excluded from the set of APs
106 | * we use to determine where the phone is at this moment.
107 | *
108 | * We do this by creating collections of emitters where all the emitters in a group
109 | * are within a plausible distance of one another. A single emitters may end up
110 | * in multiple groups. When done, we return the largest group.
111 | *
112 | * If we are at the extreme limit of possible coverage (maximumRange)
113 | * from two emitters then those emitters could be a distance of 2*maximumRange apart.
114 | * So we will group the emitters based on that large distance.
115 | *
116 | * @param locations A collection of the coverages for the current observation set
117 | * @return The largest set of coverages found within the raw observations. That is
118 | * the most believable set of coverage areas.
119 | */
120 | fun culledEmitters(locations: Collection): Set? {
121 | val groups = divideInGroups(locations)
122 | groups.maxByOrNull { it.size }?.let { result ->
123 | // if we only have one location, use it as long as it's not an invalid emitter
124 | if (locations.size == 1 && result.single().id.rfType != EmitterType.INVALID) {
125 | if (DEBUG) Log.d(TAG, "culledEmitters() - got only one location, use it")
126 | return result
127 | }
128 | // Determine minimum count for a valid group of emitters.
129 | // The RfEmitter class will have put the min count into the location
130 | // it provided.
131 | result.forEach {
132 | if (result.size >= it.id.rfType.getRfCharacteristics().minCount)
133 | return result
134 | }
135 | if (DEBUG) Log.d(TAG, "culledEmitters() - only got ${result.size}, but " +
136 | "${result.minOfOrNull { it.id.rfType.getRfCharacteristics().minCount }} are required")
137 | }
138 | return null
139 | }
140 |
141 | /**
142 | * Build a list of sets (or groups) each outer set member is a set of coverage of
143 | * reasonably near RF emitters. Basically we are grouping the raw observations
144 | * into clumps based on how believably close together they are. An outlying emitter
145 | * will likely be put into its own group. Our caller will take the largest set as
146 | * the most believable group of observations to use to compute a position.
147 | *
148 | * @param locations A set of RF emitter coverage records
149 | * @return A list of coverage sets.
150 | */
151 | private fun divideInGroups(locations: Collection): List> {
152 | // Create bins
153 | val bins = locations.map { hashSetOf(it) }
154 | for (location in locations) {
155 | for (locationGroup in bins) {
156 | if (locationCompatibleWithGroup(location, locationGroup)) {
157 | locationGroup.add(location)
158 | }
159 | }
160 | }
161 | return bins
162 | }
163 |
164 | /**
165 | * Check to see if the coverage area (location) of an RF emitter is close
166 | * enough to others in a group that we can believably add it to the group.
167 | * @param location The coverage area of the candidate emitter
168 | * @param locGroup The coverage areas of the emitters already in the group
169 | * @return True if location is close to others in group
170 | */
171 | private fun locationCompatibleWithGroup(location: RfLocation, locGroup: Set): Boolean {
172 | // If the location is within range of all current members of the
173 | // group, then we are compatible.
174 | for (other in locGroup) {
175 | // allow somewhat larger distance than sum of accuracies, looks like results are usually a bit better
176 | if (approximateDistance(location.lat, location.lon, other.lat, other.lon) > (location.accuracyEstimate + other.accuracyEstimate) * 1.25) {
177 | return false
178 | }
179 | }
180 | return true
181 | }
182 |
183 | /**
184 | * Shorter version of the original WeightedAverage, with adjusted weight to consider emitters
185 | * we don't know much about.
186 | * This ignores multiplying longitude accuracy by cosLat when converting to degrees, and
187 | * later dividing by cosLat when converting back to meters. It doesn't cancel out completely
188 | * because the used latitudes generally are slightly different, but differences are negligible
189 | * for our use.
190 | */
191 | // main difference to the old WeightedAverage: accuracy is also influenced by how far
192 | // apart the emitters are (sounds more relevant than it is, due to only "compatible" locations
193 | // being used anyway)
194 | fun Collection.weightedAverage(): Location {
195 | val latitudes = DoubleArray(size)
196 | val longitudes = DoubleArray(size)
197 | val accuracies = DoubleArray(size)
198 | val weights = DoubleArray(size)
199 | forEachIndexed { i, it ->
200 | latitudes[i] = it.lat
201 | longitudes[i] = it.lon
202 | val minRange = it.id.rfType.getRfCharacteristics().minimumRange
203 | // significantly reduce asu if we don't really trust the location, but don't discard it
204 | val asu = if (it.suspicious) (it.asu / 4).coerceAtLeast(MINIMUM_ASU) else it.asu
205 | weights[i] = asu / it.accuracyEstimate
206 |
207 | // The actual accuracy we want to use for this location is an adjusted accuracyEstimate.
208 | // If asu is good, we're likely close to the emitter, so we can decrease accuracy value.
209 | // asuAdjustedAccuracy varies between minRange and accuracyEstimate
210 | // But at the same time, we may not have the full emitter in the database.
211 | // In this case, an accuracy improvement may actually result in an over-confident estimate,
212 | // which is not desirable. Thus we reduce the asuFactor if the emitter is much smaller than
213 | // it maximum range for its type.
214 | val rangeFactor = min(5 * it.radius / it.id.rfType.getRfCharacteristics().maximumRange, 1.0)
215 | val asuFactor = 1.0 - ((asu - MINIMUM_ASU) * 1.0 / MAXIMUM_ASU) * rangeFactor
216 |
217 | val asuAdjustedAccuracy = minRange + asuFactor * asuFactor * (it.accuracyEstimate - minRange)
218 |
219 | //
220 | // Our input has an accuracy based on the detection of the edge of the coverage area.
221 | // So assume that is a high (two sigma) probability and, worse, assume we can turn that
222 | // into normal distribution error statistic. We will assume our standard deviation (one
223 | // sigma) is half of our accuracy.
224 | //accuracies[i] = asuAdjustedAccuracy * METER_TO_DEG * 0.5
225 | // But we use the factor 0.7 instead, because 0.5 sometimes gives overly accurate results.
226 | // This makes accuracy worse if asu is low, and if range is close to minRange. The former
227 | // is desired, and the latter is a side effect that usually isn't that bad
228 | accuracies[i] = asuAdjustedAccuracy * METER_TO_DEG * 0.7
229 | }
230 | // set weighted means
231 | val latMean = weightedMean(latitudes, weights)
232 | val lonMean = weightedMean(longitudes, weights)
233 | // and variances, to use for accuracy
234 | val hasWifi = any { it.id.rfType in shortRangeEmitterTypes }
235 | val latVariance = weightedVariance(latMean, latitudes, accuracies, weights, hasWifi)
236 | val lonVariance = weightedVariance(lonMean, longitudes, accuracies, weights, hasWifi)
237 | val acc = (sqrt(latVariance + lonVariance) * DEG_TO_METER)
238 | // seen weirdly bad results if only 1 emitter is available, and we only have seen it in
239 | // very few locations -> need to catch this
240 | // similar if all WiFis are suspicious... don't trust it
241 | val allWifisSuspicious = hasWifi && none { !it.suspicious && it.id.rfType in shortRangeEmitterTypes }
242 | val reportAcc = acc * if (allWifisSuspicious || (size == 1 && first().radius < single().id.rfType.getRfCharacteristics().minimumRange))
243 | 1.5 else 1.0 // factor 1.5 to approximately undo the factor 0.7 above
244 | return location(latMean, lonMean, reportAcc.toFloat())
245 | }
246 |
247 | fun Collection.location(lat: Double, lon: Double, acc: Float): Location =
248 | Location(LOCATION_PROVIDER).apply {
249 | extras = Bundle().apply { putInt("AVERAGED_OF", size) }
250 |
251 | // set newest times
252 | time = maxOf { it.time }
253 | elapsedRealtimeNanos = maxOf { it.elapsedRealtimeNanos }
254 |
255 | latitude = lat
256 | longitude = lon
257 | accuracy = acc.coerceAtLeast(MINIMUM_BELIEVABLE_ACCURACY)
258 | }
259 |
260 | /**
261 | * @returns the weighted mean of the given positions, accuracies and weights
262 | */
263 | private fun weightedMean(positions: DoubleArray, weights: DoubleArray): Double {
264 | var weightedSum = 0.0
265 | positions.forEachIndexed { i, position ->
266 | weightedSum += position * weights[i]
267 | }
268 | return weightedSum / weights.sum()
269 | }
270 |
271 | /**
272 | * @returns the weighted variance of the given positions, accuracies and weights.
273 | * Variance and not stdDev because we need to square it anyway
274 | *
275 | * Actually this is not really correct, but it's good enough...
276 | * What we want from accuracy:
277 | * more (very) similar locations should improve accuracy
278 | * positions far apart should give worse accuracy, even if the single accuracies are similar
279 | */
280 | private fun weightedVariance(weightedMeanPosition: Double, positions: DoubleArray, accuracies: DoubleArray, weights: DoubleArray, betterAccuracy: Boolean): Double {
281 | // we have a situation like
282 | // https://stats.stackexchange.com/questions/454120/how-can-i-calculate-uncertainty-of-the-mean-of-a-set-of-samples-with-different-u#comment844099_454266
283 | // but we already have weights... so come up with something that gives reasonable results
284 | var weightedVarianceSum = 0.0
285 | positions.forEachIndexed { i, position ->
286 | weightedVarianceSum += if (betterAccuracy) {
287 | // usually 5-20% better accuracy, but often not nice if we don't have any wifis
288 | val dev = max(accuracies[i], abs(position - weightedMeanPosition))
289 | weights[i] * weights[i] * dev * dev
290 | } else
291 | weights[i] * weights[i] * (accuracies[i] * accuracies[i] + (position - weightedMeanPosition) * (position - weightedMeanPosition))
292 | }
293 |
294 | // this is not really variance, but still similar enough to claim it is
295 | // dividing by size should be fine...
296 | return weightedVarianceSum / weights.sumOf { it * it }
297 | }
298 |
299 | // weighted average with removing outliers (more than 2 accuracies away from median center)
300 | // and use only short range emitters if any are available
301 | fun Collection.medianCull(): Collection? {
302 | if (isEmpty()) return null
303 | // use trustworthy wifi results for median location, but only if at least 3 emitters
304 | // if we have less than 3 results, also use suspicious results
305 | // if we still have less than 3 results, use all
306 | // 3 results because with less there is a too high chance of bad median locations (see below)
307 | val emittersForMedian = filter { it.id.rfType in shortRangeEmitterTypes && !it.suspicious }
308 | .let { goodList ->
309 | if (goodList.size >= 3) goodList
310 | else this.filter { it.id.rfType in shortRangeEmitterTypes }
311 | .let { okList ->
312 | if (okList.size >= 3) okList
313 | else this
314 | }
315 | }
316 | // Take median of lat and lon separately because it simple. This can lead to unexpected and
317 | // bad results if emitters are very far apart. Ideally such cases should be caught in medianCullSafe.
318 | val latMedian = emittersForMedian.map { it.lat }.median()
319 | val lonMedian = emittersForMedian.map { it.lon }.median()
320 | // Use locations that are close enough to the median location (2 * their accuracy).
321 | // Maybe the factor 2 could be reduced to 1.5 or sth like this... but we really just want to
322 | // remove outliers, so it shouldn't matter too much.
323 | val closeToMedian = filter { approximateDistance(latMedian, lonMedian, it.lat, it.lon) < 2.0 * it.accuracyEstimate }
324 | if (DEBUG) Log.d(TAG, "medianCull() - using ${closeToMedian.size} of initially $size locations")
325 | return closeToMedian.ifEmpty { culledEmitters(this) } // fallback to original culledEmitters
326 | }
327 |
328 | private fun List.median() = sorted().let {
329 | if (size % 2 == 1) it[size / 2]
330 | else (it[size / 2] + it[(size - 1) / 2]) / 2
331 | }
332 |
333 | fun Collection.medianCullSafe(): Location? {
334 | val medianCull = medianCull() ?: return null // returns null if list is empty
335 | /* Need to decide whether to really use medianCull, because in some cases it produces
336 | * bad results. To detect such cases we use a more exhaustive check if :
337 | * a. Any locations have been removed, and the resulting locations does not fit with noCullLoc,
338 | * i.e. they are further apart than the smaller accuracy
339 | * b. Too many locations have been removed. This can happen if medianCullLoc is at some
340 | * bad location, e.g. between 2 WiFi groups, or it's messed up because lat and lon
341 | * are treated independently in medianCull()
342 | * c. All WiFi emitters have been removed. This should not happen, but still does in some cases
343 | * like when we have a single WiFi that is far away from mobile emitters
344 | * If any check returns true, we also create normalCullLoc and use whichever of the three
345 | * locations is closest to their center.
346 | */
347 | if (medianCull.size == size) return this.weightedAverage() // nothing removed, all should be fine
348 | val medianCullLoc = medianCull.weightedAverage()
349 | val noCullLoc = weightedAverage()
350 | val d = approximateDistance(medianCullLoc.latitude, medianCullLoc.longitude, noCullLoc.latitude, noCullLoc.longitude)
351 | if (d > medianCullLoc.accuracy
352 | || d > noCullLoc.accuracy
353 | || medianCull.size <= size * 0.8
354 | || (medianCull.none { it.id.rfType in shortRangeEmitterTypes } && this.any { it.id.rfType in shortRangeEmitterTypes })
355 | ) {
356 | // we have a potentially bad location -> check normal cull and no cull and compare
357 | val normalCullLoc = culledEmitters(this)?.weightedAverage()
358 | val locs = listOfNotNull(medianCullLoc, noCullLoc, normalCullLoc)
359 | val meanLat = locs.sumOf { it.latitude } / locs.size
360 | val meanLon = locs.sumOf { it.longitude } / locs.size
361 | val l = locs.minByOrNull {
362 | approximateDistance(meanLat, meanLon, it.latitude, it.longitude)
363 | }
364 | // this very often results in noCull, which may be much less accurate than the other 2
365 | // so try using medianCull location instead if it seems reasonably accurate
366 | if (l == noCullLoc && noCullLoc.accuracy > 2.0 * medianCullLoc.accuracy
367 | && approximateDistance(noCullLoc.latitude, noCullLoc.longitude, medianCullLoc.latitude, medianCullLoc.longitude) < noCullLoc.accuracy
368 | ) {
369 | if (DEBUG) Log.d(TAG, "medianCullSafe() - using medianCull because chosen noCull is close but much less accurate")
370 | return medianCullLoc
371 | }
372 | if (DEBUG) {
373 | if (l == medianCullLoc)
374 | Log.d(TAG, "medianCullSafe() - checked medianCull, still using")
375 | else
376 | Log.d(TAG, "medianCullSafe() - not using medianCull")
377 | }
378 | return l
379 | }
380 | return medianCullLoc
381 | }
382 |
--------------------------------------------------------------------------------
/app/src/main/java/org/fitchfamily/android/dejavu/Database.kt:
--------------------------------------------------------------------------------
1 | package org.fitchfamily.android.dejavu
2 |
3 | /*
4 | * Local NLP Backend / DejaVu - A location provider backend for microG/UnifiedNlp
5 | *
6 | * Copyright (C) 2017 Tod Fitch
7 | * Copyright (C) 2022 Helium314
8 | *
9 | * This program is Free Software: you can redistribute it and/or modify
10 | * it under the terms of the GNU General Public License as
11 | * published by the Free Software Foundation, either version 3 of the
12 | * License, or (at your option) any later version.
13 | *
14 | * This program is distributed in the hope that it will be useful,
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | * GNU General Public License for more details.
18 | *
19 | * You should have received a copy of the GNU General Public License
20 | * along with this program. If not, see .
21 | */
22 |
23 | import android.annotation.SuppressLint
24 | import android.content.ContentValues
25 | import android.content.Context
26 | import android.database.Cursor
27 | import android.database.DatabaseUtils
28 | import android.database.sqlite.SQLiteDatabase
29 | import android.database.sqlite.SQLiteOpenHelper
30 | import android.util.Log
31 |
32 | /**
33 | *
34 | * Created by tfitch on 9/1/17.
35 | * modified by helium314 in 2022
36 | */
37 | /**
38 | * Interface to our on flash SQL database. Note that these methods are not
39 | * thread safe. However all access to the database is through the Cache object
40 | * which is thread safe.
41 | */
42 | class Database(context: Context?, name: String = DB_NAME) : // allow overriding name, useful for importing db
43 | SQLiteOpenHelper(context, name, null, VERSION) {
44 | private val database: SQLiteDatabase get() = writableDatabase
45 | private var withinTransaction = false
46 | private var updatesMade = false
47 |
48 | override fun onCreate(db: SQLiteDatabase) {
49 | withinTransaction = false
50 | // Always create version 1 of database, then update the schema
51 | // in the same order it might occur "in the wild". Avoids having
52 | // to check to see if the table exists (may be old version)
53 | // or not (can be new version).
54 | db.execSQL("""
55 | CREATE TABLE IF NOT EXISTS $TABLE_SAMPLES (
56 | $COL_RFID STRING PRIMARY KEY,
57 | $COL_TYPE STRING,
58 | $OLD_COL_TRUST INTEGER,
59 | $COL_LAT REAL,
60 | $COL_LON REAL,
61 | $OLD_COL_RAD REAL,
62 | $COL_NOTE STRING
63 | );
64 | """.trimIndent()
65 | )
66 | onUpgrade(db, 1, VERSION)
67 | }
68 |
69 | @SuppressLint("Recycle") // cursor is closed in toSequence
70 | private fun query(
71 | columns: Array? = null,
72 | where: String? = null,
73 | args: Array? = null,
74 | groupBy: String? = null,
75 | having: String? = null,
76 | orderBy: String? = null,
77 | limit: String? = null,
78 | distinct: Boolean = false,
79 | transform: (CursorPosition) -> T
80 | ): Sequence {
81 | return database.query(distinct, TABLE_SAMPLES, columns, where, args, groupBy, having, orderBy, limit).toSequence(transform)
82 | }
83 |
84 | override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
85 | if (oldVersion < 2) upGradeToVersion2(db)
86 | if (oldVersion < 3) upGradeToVersion3(db)
87 | if (oldVersion < 4) upGradeToVersion4(db)
88 | }
89 |
90 | @SuppressLint("SQLiteString") // issue is known and fixed later, but keep this old code exactly as it was
91 | private fun upGradeToVersion2(db: SQLiteDatabase) {
92 | if (DEBUG) Log.d(TAG, "upGradeToVersion2(): Entry")
93 | // Sqlite3 does not support dropping columns so we create a new table with our
94 | // current fields and copy the old data into it.
95 | with(db) {
96 | execSQL("BEGIN TRANSACTION;")
97 | execSQL("ALTER TABLE " + TABLE_SAMPLES + " RENAME TO " + TABLE_SAMPLES + "_old;")
98 | execSQL(
99 | ("CREATE TABLE IF NOT EXISTS " + TABLE_SAMPLES + "(" +
100 | COL_RFID + " STRING PRIMARY KEY, " +
101 | COL_TYPE + " STRING, " +
102 | OLD_COL_TRUST + " INTEGER, " +
103 | COL_LAT + " REAL, " +
104 | COL_LON + " REAL, " +
105 | COL_RAD_NS + " REAL, " +
106 | COL_RAD_EW + " REAL, " +
107 | COL_NOTE + " STRING);")
108 | )
109 | execSQL(
110 | ("INSERT INTO " + TABLE_SAMPLES + "(" +
111 | COL_RFID + ", " +
112 | COL_TYPE + ", " +
113 | OLD_COL_TRUST + ", " +
114 | COL_LAT + ", " +
115 | COL_LON + ", " +
116 | COL_RAD_NS + ", " +
117 | COL_RAD_EW + ", " +
118 | COL_NOTE +
119 | ") SELECT " +
120 | COL_RFID + ", " +
121 | COL_TYPE + ", " +
122 | OLD_COL_TRUST + ", " +
123 | COL_LAT + ", " +
124 | COL_LON + ", " +
125 | OLD_COL_RAD + ", " +
126 | OLD_COL_RAD + ", " +
127 | COL_NOTE +
128 | " FROM " + TABLE_SAMPLES + "_old;")
129 | )
130 | execSQL("DROP TABLE " + TABLE_SAMPLES + "_old;")
131 | execSQL("COMMIT;")
132 | }
133 | }
134 |
135 | private fun upGradeToVersion3(db: SQLiteDatabase) {
136 | if (DEBUG) Log.d(TAG, "upGradeToVersion3(): Entry")
137 |
138 | // We are changing our key field to a new text field that contains a hash of
139 | // of the ID and type. In addition, we are dealing with a Lint complaint about
140 | // using a string field where we ought to be using a text field.
141 | db.execSQL("BEGIN TRANSACTION;")
142 | db.execSQL(
143 | ("CREATE TABLE IF NOT EXISTS " + TABLE_SAMPLES + "_new (" +
144 | OLD_COL_HASH + " TEXT PRIMARY KEY, " +
145 | COL_RFID + " TEXT, " +
146 | COL_TYPE + " TEXT, " +
147 | OLD_COL_TRUST + " INTEGER, " +
148 | COL_LAT + " REAL, " +
149 | COL_LON + " REAL, " +
150 | COL_RAD_NS + " REAL, " +
151 | COL_RAD_EW + " REAL, " +
152 | COL_NOTE + " TEXT);")
153 | )
154 | val insert = db.compileStatement(
155 | ("INSERT INTO " +
156 | TABLE_SAMPLES + "_new(" +
157 | OLD_COL_HASH + ", " +
158 | COL_RFID + ", " +
159 | COL_TYPE + ", " +
160 | OLD_COL_TRUST + ", " +
161 | COL_LAT + ", " +
162 | COL_LON + ", " +
163 | COL_RAD_NS + ", " +
164 | COL_RAD_EW + ", " +
165 | COL_NOTE + ") " +
166 | "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);")
167 | )
168 | val query = ("SELECT " +
169 | COL_RFID + "," + COL_TYPE + "," + OLD_COL_TRUST + "," + COL_LAT + "," + COL_LON + "," + COL_RAD_NS + "," + COL_RAD_EW + "," + COL_NOTE + " " +
170 | "FROM " + TABLE_SAMPLES + ";")
171 | db.rawQuery(query, null).use { cursor ->
172 | if (cursor!!.moveToFirst()) {
173 | do {
174 | val rfId = cursor.getString(0)
175 | var rftype = cursor.getString(1)
176 | if ((rftype == "WLAN")) rftype = "WLAN_24GHZ"
177 | val hash = rfId + rftype // value doesn't matter, it's removed in next upgrade anyway
178 |
179 | // Log.d(TAG,"upGradeToVersion2(): Updating '"+rfId.toString()+"'");
180 | insert.bindString(1, hash)
181 | insert.bindString(2, rfId)
182 | insert.bindString(3, rftype)
183 | insert.bindString(4, cursor.getString(2))
184 | insert.bindString(5, cursor.getString(3))
185 | insert.bindString(6, cursor.getString(4))
186 | insert.bindString(7, cursor.getString(5))
187 | insert.bindString(8, cursor.getString(6))
188 | insert.bindString(9, cursor.getString(7))
189 | insert.executeInsert()
190 | insert.clearBindings()
191 | } while (cursor.moveToNext())
192 | }
193 | }
194 | db.execSQL("DROP TABLE $TABLE_SAMPLES;")
195 | db.execSQL("ALTER TABLE ${TABLE_SAMPLES}_new RENAME TO $TABLE_SAMPLES;")
196 | db.execSQL("COMMIT;")
197 | }
198 |
199 | private fun upGradeToVersion4(db: SQLiteDatabase) {
200 | // We replace the rfId hash with the actual rfId
201 | // mobile emitter IDs are already unique
202 | // WiFi emitters get WiFi type prefixed
203 | // Trust column is removed, like the whole trust system
204 | db.execSQL("BEGIN TRANSACTION;")
205 | db.execSQL("""
206 | CREATE TABLE IF NOT EXISTS ${TABLE_SAMPLES}_new (
207 | $COL_RFID TEXT PRIMARY KEY NOT NULL,
208 | $COL_TYPE TEXT NOT NULL,
209 | $COL_LAT REAL NOT NULL,
210 | $COL_LON REAL NOT NULL,
211 | $COL_RAD_NS REAL NOT NULL,
212 | $COL_RAD_EW REAL NOT NULL,
213 | $COL_NOTE TEXT
214 | );
215 | """.trimIndent()
216 | )
217 | // add 2.4 GHz WiFis
218 | db.execSQL("""
219 | INSERT INTO ${TABLE_SAMPLES}_new($COL_RFID, $COL_TYPE, $COL_LAT, $COL_LON, $COL_RAD_NS, $COL_RAD_EW, $COL_NOTE)
220 | SELECT '${EmitterType.WLAN2}/' || $COL_RFID, '${EmitterType.WLAN2}', $COL_LAT, $COL_LON, $COL_RAD_NS, $COL_RAD_EW, $COL_NOTE
221 | FROM $TABLE_SAMPLES
222 | WHERE $COL_TYPE = 'WLAN_24GHZ';
223 | """.trimIndent()
224 | )
225 | // add 5 GHz WiFis
226 | db.execSQL("""
227 | INSERT INTO ${TABLE_SAMPLES}_new($COL_RFID, $COL_TYPE, $COL_LAT, $COL_LON, $COL_RAD_NS, $COL_RAD_EW, $COL_NOTE)
228 | SELECT '${EmitterType.WLAN5}/' || $COL_RFID, '${EmitterType.WLAN5}', $COL_LAT, $COL_LON, $COL_RAD_NS, $COL_RAD_EW, $COL_NOTE
229 | FROM $TABLE_SAMPLES
230 | WHERE $COL_TYPE = 'WLAN_5GHZ';
231 | """.trimIndent()
232 | )
233 | // cell towers are already unique, but we need to split the types, as they may have different characteristics
234 | for (emitterType in arrayOf(EmitterType.GSM, EmitterType.WCDMA, EmitterType.CDMA, EmitterType.LTE)) {
235 | db.execSQL("""
236 | INSERT INTO ${TABLE_SAMPLES}_new($COL_RFID, $COL_TYPE, $COL_LAT, $COL_LON, $COL_RAD_NS, $COL_RAD_EW, $COL_NOTE)
237 | SELECT $COL_RFID, '${emitterType}', $COL_LAT, $COL_LON, $COL_RAD_NS, $COL_RAD_EW, $COL_NOTE
238 | FROM $TABLE_SAMPLES
239 | WHERE $COL_TYPE = 'MOBILE' AND $COL_RFID LIKE '${emitterType}%';
240 | """.trimIndent()
241 | )
242 | }
243 | db.execSQL("DROP TABLE $TABLE_SAMPLES;")
244 | db.execSQL("ALTER TABLE ${TABLE_SAMPLES}_new RENAME TO $TABLE_SAMPLES;")
245 | db.execSQL("COMMIT;")
246 | }
247 |
248 | override fun onOpen(db: SQLiteDatabase) {
249 | super.onOpen(db)
250 | if (databaseName == DB_NAME)
251 | instance = this
252 | }
253 |
254 | override fun close() {
255 | if (databaseName == DB_NAME)
256 | instance = null
257 | super.close()
258 | }
259 |
260 | /**
261 | * Start an update operation.
262 | */
263 | fun beginTransaction() {
264 | if (withinTransaction) {
265 | if (DEBUG) Log.d(TAG, "beginTransaction() - Already in a transaction?")
266 | return
267 | }
268 | withinTransaction = true
269 | updatesMade = false
270 | database.beginTransaction()
271 | }
272 |
273 | /**
274 | * End a transaction. If we actually made any changes then we mark
275 | * the transaction as successful. Once marked as successful we
276 | * end the transaction with the underlying SQL database.
277 | */
278 | fun endTransaction() {
279 | if (!withinTransaction) {
280 | if (DEBUG) Log.d(TAG, "Asked to end transaction but we are not in one???")
281 | return
282 | }
283 | if (updatesMade)
284 | database.setTransactionSuccessful()
285 | updatesMade = false
286 | database.endTransaction()
287 | withinTransaction = false
288 | }
289 |
290 | /**
291 | * End a transaction without marking it as successful.
292 | */
293 | fun cancelTransaction() {
294 | if (!withinTransaction) {
295 | if (DEBUG) Log.d(TAG, "Asked to end transaction but we are not in one???")
296 | return
297 | }
298 | updatesMade = false
299 | database.endTransaction()
300 | withinTransaction = false
301 | }
302 |
303 | /**
304 | * Drop an RF emitter from the database.
305 | *
306 | * @param emitter The emitter to be dropped.
307 | */
308 | fun drop(emitter: RfEmitter) {
309 | if (DEBUG) Log.d(TAG, "Dropping " + emitter.logString + " from db")
310 | database.delete(TABLE_SAMPLES, "$COL_RFID = '${emitter.uniqueId}'", null)
311 | updatesMade = true
312 | }
313 |
314 | /**
315 | * Insert a new RF emitter into the database.
316 | *
317 | * @param emitter The emitter to be added.
318 | */
319 | fun insert(emitter: RfEmitter, collision: Int = SQLiteDatabase.CONFLICT_ABORT) {
320 | val cv = ContentValues(7).apply {
321 | put(COL_RFID, emitter.uniqueId)
322 | put(COL_TYPE, emitter.type.toString())
323 | put(COL_LAT, emitter.lat)
324 | put(COL_LON, emitter.lon)
325 | put(COL_RAD_NS, emitter.radiusNS)
326 | put(COL_RAD_EW, emitter.radiusEW)
327 | put(COL_NOTE, emitter.note)
328 | }
329 | insertWithCollision(cv, collision)
330 | }
331 |
332 | fun insertLine(collision: Int, rfId: String, type: String, lat: Double, lon: Double, radius_ns: Double, radius_ew: Double, note: String) {
333 | val cv = ContentValues(7).apply {
334 | put(COL_RFID, rfId)
335 | put(COL_TYPE, type)
336 | put(COL_LAT, lat)
337 | put(COL_LON, lon)
338 | put(COL_RAD_NS, radius_ns)
339 | put(COL_RAD_EW, radius_ew)
340 | put(COL_NOTE, note)
341 | }
342 | insertWithCollision(cv, collision)
343 | }
344 |
345 | private fun insertWithCollision(cv: ContentValues, collision: Int) {
346 | if (DEBUG) Log.d(TAG, "Inserting $cv into db with collision $collision")
347 | if (collision == COLLISION_MERGE && database.insertWithOnConflict(TABLE_SAMPLES, null, cv, SQLiteDatabase.CONFLICT_IGNORE) == -1L) { // -1 is returned if a conflict is detected
348 | // trying to insert, but row exists and we want to merge
349 | val bboxOld = query(arrayOf(COL_LAT, COL_LON, COL_RAD_NS, COL_RAD_EW), "$COL_RFID = '${cv.getAsString(COL_RFID)}'", limit = "1") {
350 | val ew = it.getDouble(COL_RAD_EW)
351 | if (ew < 0) null
352 | else BoundingBox(it.getDouble(COL_LAT), it.getDouble(COL_LON), it.getDouble(COL_RAD_NS), ew)
353 | }.firstOrNull()
354 | val bboxNew = BoundingBox(cv.getAsDouble(COL_LAT), cv.getAsDouble(COL_LON), cv.getAsDouble(COL_RAD_NS), cv.getAsDouble(COL_RAD_EW))
355 | if (bboxNew == bboxOld) return
356 | if (bboxOld != null) {
357 | bboxNew.update(bboxOld.south, bboxOld.east)
358 | bboxNew.update(bboxOld.north, bboxOld.west)
359 | }
360 | val cvUpdate = ContentValues(4).apply {
361 | put(COL_LAT, bboxNew.center_lat)
362 | put(COL_LON, bboxNew.center_lon)
363 | put(COL_RAD_NS, bboxNew.radius_ns)
364 | put(COL_RAD_EW, bboxNew.radius_ew)
365 | }
366 | database.update(TABLE_SAMPLES, cvUpdate, "$COL_RFID = '${cv.getAsString(COL_RFID)}'", null)
367 | } else if (collision != COLLISION_MERGE)
368 | database.insertWithOnConflict(TABLE_SAMPLES, null, cv, collision)
369 | updatesMade = true
370 | }
371 |
372 | fun setInvalid(emitter: RfEmitter) {
373 | if (DEBUG) Log.d(TAG, "Setting to invalid: " + emitter.logString)
374 | database.update(
375 | TABLE_SAMPLES,
376 | ContentValues(2).apply {
377 | put(COL_RAD_NS, -1.0)
378 | put(COL_RAD_EW, -1.0)
379 | },
380 | "$COL_RFID = '${emitter.uniqueId}'",
381 | null
382 | )
383 | updatesMade = true
384 | }
385 |
386 | /**
387 | * Update information about an emitter already existing in the database
388 | *
389 | * @param emitter The emitter to be updated
390 | */
391 | fun update(emitter: RfEmitter) {
392 | if (DEBUG) Log.d(TAG, "Updating " + emitter.logString)
393 | val cv = ContentValues(5).apply {
394 | put(COL_LAT, emitter.lat)
395 | put(COL_LON, emitter.lon)
396 | put(COL_RAD_NS, emitter.radiusNS)
397 | put(COL_RAD_EW, emitter.radiusEW)
398 | put(COL_NOTE, emitter.note)
399 | }
400 | database.update(TABLE_SAMPLES, cv, "$COL_RFID = '${emitter.uniqueId}'", null)
401 | updatesMade = true
402 | }
403 |
404 | /**
405 | * Get all the information we have on a single RF emitter
406 | *
407 | * @param rfId The identification of the emitter caller wants
408 | * @return A emitter object with all the information we have. Or null if we have nothing.
409 | */
410 | fun getEmitter(rfId: RfIdentification) =
411 | query(
412 | arrayOf(COL_LAT, COL_LON, COL_RAD_NS, COL_RAD_EW, COL_NOTE),
413 | "$COL_RFID = '${rfId.uniqueId}'",
414 | limit = "1"
415 | ) { it.toRfEmitter(rfId) }.firstOrNull()
416 |
417 | // get multiple emitters instead of querying one by one
418 | fun getEmitters(rfIds: Collection): List {
419 | val idString = rfIds.joinToString(",") { "'${it.uniqueId}'" }
420 | return query(allColumns, "$COL_RFID IN ($idString)") { it.toRfEmitter() }.filterNotNull().toList()
421 | }
422 |
423 | fun getAll() = query(allColumns) { it.toRfEmitter() }.filterNotNull()
424 |
425 | fun getSize() = DatabaseUtils.queryNumEntries(database, TABLE_SAMPLES)
426 |
427 | companion object {
428 | var instance: Database? = null
429 | private set
430 | }
431 | }
432 |
433 | private const val TAG = "LocalNLP DB"
434 | private val DEBUG = BuildConfig.DEBUG
435 |
436 | private const val DB_NAME = "rf.db"
437 | private const val TABLE_SAMPLES = "emitters"
438 | private const val VERSION = 4
439 | const val COL_TYPE = "rfType"
440 | const val COL_RFID = "rfID"
441 | const val COL_LAT = "latitude"
442 | const val COL_LON = "longitude"
443 | const val COL_RAD_NS = "radius_ns" // v2 of database
444 | const val COL_RAD_EW = "radius_ew" // v2 of database
445 | const val COL_NOTE = "note"
446 | // columns used in old db versions
447 | private const val OLD_COL_HASH = "rfHash" // v3 of database, removed in v4
448 | private const val OLD_COL_TRUST = "trust" // removed in v4
449 | private const val OLD_COL_RAD = "radius" // v1 of database
450 |
451 | const val COLLISION_MERGE = 0 // merge emitters on collision when inserting
452 |
453 | private val allColumns = arrayOf(COL_RFID, COL_TYPE, COL_LAT, COL_LON, COL_RAD_NS, COL_RAD_EW, COL_NOTE)
454 | private val wifis = hashSetOf(EmitterType.WLAN2, EmitterType.WLAN5, EmitterType.WLAN6)
455 |
456 | class EmitterInfo(
457 | val latitude: Double,
458 | val longitude: Double,
459 | val radius_ns: Double,
460 | val radius_ew: Double,
461 | val note: String
462 | )
463 |
464 | private class CursorPosition(private val cursor: Cursor) {
465 | fun getDouble(columnName: String): Double = cursor.getDouble(index(columnName))
466 | fun getString(columnName: String): String = cursor.getString(index(columnName))
467 |
468 | private fun index(columnName: String): Int = cursor.getColumnIndexOrThrow(columnName)
469 | }
470 |
471 | private inline fun Cursor.toSequence(crossinline transform: (CursorPosition) -> T): Sequence {
472 | val c = CursorPosition(this)
473 | moveToFirst()
474 | return generateSequence {
475 | if (!isAfterLast) {
476 | val r = transform(c)
477 | moveToNext()
478 | r
479 | } else {
480 | close()
481 | null
482 | }
483 | }
484 | }
485 |
486 | private fun CursorPosition.toRfEmitter(rfId: RfIdentification? = null): RfEmitter? {
487 | val info = EmitterInfo(getDouble(COL_LAT), getDouble(COL_LON), getDouble(COL_RAD_NS), getDouble(COL_RAD_EW), getString(COL_NOTE))
488 | return if (rfId == null) {
489 | val type = try {
490 | EmitterType.valueOf(getString(COL_TYPE))
491 | } catch (_: Exception) {
492 | return null
493 | }
494 | val dbId = getString(COL_RFID)
495 | val id = if (type in wifis) dbId.substringAfter('/')
496 | else dbId
497 | RfEmitter(type, id, info)
498 | } else
499 | RfEmitter(rfId, info)
500 | }
501 |
--------------------------------------------------------------------------------
/app/src/main/java/org/fitchfamily/android/dejavu/RfEmitter.kt:
--------------------------------------------------------------------------------
1 | package org.fitchfamily.android.dejavu
2 |
3 | /*
4 | * Local NLP Backend / DejaVu - A location provider backend for microG/UnifiedNlp
5 | *
6 | * Copyright (C) 2017 Tod Fitch
7 | * Copyright (C) 2023 Helium314
8 | *
9 | * This program is Free Software: you can redistribute it and/or modify
10 | * it under the terms of the GNU General Public License as
11 | * published by the Free Software Foundation, either version 3 of the
12 | * License, or (at your option) any later version.
13 | *
14 | * This program is distributed in the hope that it will be useful,
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | * GNU General Public License for more details.
18 | *
19 | * You should have received a copy of the GNU General Public License
20 | * along with this program. If not, see .
21 | */
22 |
23 | import android.location.Location
24 | import android.util.Log
25 | import org.fitchfamily.android.dejavu.EmitterType.*
26 | import kotlin.math.abs
27 |
28 | /**
29 | * Created by tfitch on 8/27/17.
30 | * modified by helium314 in 2022
31 | */
32 | /**
33 | * Models everything we know about an RF emitter: Its identification, most recently received
34 | * signal level, an estimate of its coverage (center point and radius), etc.
35 | *
36 | * Starting with v2 of the database, we store a north-south radius and an east-west radius which
37 | * allows for a rectangular bounding box rather than a square one.
38 | *
39 | * When an RF emitter is first observed we create a new object and, if information exists in
40 | * the database, populate it from saved information.
41 | *
42 | * Periodically we sync our current information about the emitter back to the flash memory
43 | * based storage.
44 | */
45 | class RfEmitter(val type: EmitterType, val id: String) {
46 | internal constructor(identification: RfIdentification) : this(identification.rfType, identification.rfId)
47 |
48 | internal constructor(identification: RfIdentification, emitterInfo: EmitterInfo) : this(identification.rfType, identification.rfId, emitterInfo)
49 |
50 | internal constructor(type: EmitterType, id: String, emitterInfo: EmitterInfo) : this(type, id) {
51 | if (emitterInfo.radius_ew < 0) {
52 | coverage = null
53 | status = EmitterStatus.STATUS_BLACKLISTED
54 | } else {
55 | coverage = BoundingBox(emitterInfo)
56 | status = EmitterStatus.STATUS_CACHED
57 | }
58 | note = emitterInfo.note
59 | // this is only for emitters that were created using old versions, with new ones too large emitters can't be in db
60 | if (emitterInfo.radius_ew > type.getRfCharacteristics().maximumRange || emitterInfo.radius_ns > type.getRfCharacteristics().maximumRange)
61 | changeStatus(EmitterStatus.STATUS_BLACKLISTED, "$logString: loaded from db, but radius too large")
62 | }
63 |
64 | private val ourCharacteristics = type.getRfCharacteristics()
65 | var coverage: BoundingBox? = null // null for new or blacklisted emitters
66 | var note: String = ""
67 | set(value) {
68 | if (field == value)
69 | return
70 | field = value
71 | if (isBlacklisted())
72 | changeStatus(EmitterStatus.STATUS_BLACKLISTED, "$logString: emitter blacklisted")
73 | }
74 | var lastObservation: Observation? = null // null if we haven't seen this emitter
75 | set(value) {
76 | field = value
77 | note = value?.note ?: ""
78 | }
79 | var status: EmitterStatus = EmitterStatus.STATUS_UNKNOWN
80 | private set
81 |
82 | val uniqueId: String get() = rfIdentification.uniqueId
83 | val rfIdentification: RfIdentification = RfIdentification(id, type)
84 | val lat: Double get() = coverage?.center_lat ?: 0.0
85 | val lon: Double get() = coverage?.center_lon ?: 0.0
86 | private val radius: Double get() = coverage?.radius ?: 0.0
87 | val radiusNS: Double get() = coverage?.radius_ns ?: 0.0
88 | val radiusEW: Double get() = coverage?.radius_ew ?: 0.0
89 |
90 | /**
91 | * All RfEmitter objects are managed through a cache. The cache needs ages out
92 | * emitters that have not been seen (or used) in a while. To do that it needs
93 | * to maintain age information for each RfEmitter object. Having the RfEmitter
94 | * object itself store the cache age is a bit of a hack, but we do it anyway.
95 | *
96 | * @return The current cache age (number of periods since last observation).
97 | */
98 | var age = 0
99 | private set
100 |
101 | /**
102 | * On equality check, we only check that our type and ID match as that
103 | * uniquely identifies our RF emitter.
104 | *
105 | * @param other The object to check for equality
106 | * @return True if the objects should be considered the same.
107 | */
108 | override fun equals(other: Any?): Boolean {
109 | if (this === other) return true
110 | if (other is RfEmitter) return rfIdentification == other.rfIdentification
111 | if (other is RfIdentification) return rfIdentification == other
112 | return false
113 | }
114 |
115 | /**
116 | * Hash code is used to determine unique objects. Our "uniqueness" is
117 | * based on which "real life" RF emitter we model, not our current
118 | * coverage, etc. So our hash code should be the same as the hash
119 | * code of our identification.
120 | *
121 | * @return A hash code for this object.
122 | */
123 | override fun hashCode(): Int {
124 | return rfIdentification.hashCode()
125 | }
126 |
127 | /**
128 | * Resets the cache age to zero.
129 | */
130 | fun resetAge() {
131 | age = 0
132 | }
133 |
134 | /**
135 | * Increment the cache age for this object.
136 | */
137 | fun incrementAge() {
138 | age++
139 | }
140 |
141 | /**
142 | * Periodically the cache sync's all dirty objects to the flash database.
143 | * This routine is called by the cache to determine if it needs to be sync'd.
144 | *
145 | * @return True if this RfEmitter needs to be written to flash.
146 | */
147 | fun syncNeeded(): Boolean {
148 | return (status == EmitterStatus.STATUS_NEW
149 | || status == EmitterStatus.STATUS_CHANGED
150 | || (status == EmitterStatus.STATUS_BLACKLISTED
151 | && coverage != null)
152 | )
153 | }
154 |
155 | /**
156 | * Synchronize this object to the flash based database. This method is called
157 | * by the cache when it is an appropriate time to assure the flash based
158 | * database is up to date with our current coverage, etc.
159 | *
160 | * @param db The database we should write our data to.
161 | */
162 | fun sync(db: Database) {
163 | if (location == null)
164 | status = EmitterStatus.STATUS_UNKNOWN
165 | var newStatus = status
166 | when (status) {
167 | EmitterStatus.STATUS_UNKNOWN -> { }
168 | EmitterStatus.STATUS_BLACKLISTED ->
169 | // If our coverage value is not null it implies that we exist in the
170 | // database as "normal" emitter. If so we ought to either remove the entry (for
171 | // blacklisted SSIDs) or set invalid radius (for too large coverage).
172 | if (coverage != null) {
173 | if (isBlacklisted()) {
174 | db.drop(this)
175 | if (DEBUG) Log.d(TAG, "sync('$logString') - Blacklisted dropping from database.")
176 | } else {
177 | db.setInvalid(this)
178 | if (DEBUG) Log.d(TAG, "sync('$logString') - Blacklisted setting to invalid, radius too large: $radius, $radiusEW, $radiusNS.")
179 | }
180 | coverage = null
181 | }
182 | EmitterStatus.STATUS_NEW -> {
183 | // Not in database, we have location. Add to database
184 | db.insert(this)
185 | newStatus = EmitterStatus.STATUS_CACHED
186 | }
187 | EmitterStatus.STATUS_CHANGED -> {
188 | // In database but we have changes
189 | db.update(this)
190 | newStatus = EmitterStatus.STATUS_CACHED
191 | }
192 | EmitterStatus.STATUS_CACHED -> { }
193 | }
194 | changeStatus(newStatus, "sync('$logString')")
195 | }
196 |
197 | val logString get() = if (DEBUG) "RF Emitter: Type=$type, ID='$id', Note='$note'" else ""
198 |
199 | /**
200 | * Update our estimate of the coverage and location of the emitter based on a
201 | * position report from the GPS system.
202 | *
203 | * @param gpsLoc A position report from a trusted (non RF emitter) source
204 | */
205 | fun updateLocation(gpsLoc: Location) {
206 | if (status == EmitterStatus.STATUS_BLACKLISTED) return
207 | val l = lastObservation ?: return // can't update emitters we haven't seen
208 | val cov = coverage
209 | // determine whether emitter will grow to unrealistic size if updated
210 | val tooLarge = if (cov != null) approximateDistance(gpsLoc.latitude, gpsLoc.longitude, cov.center_lat, cov.center_lon) > (type.getRfCharacteristics().maximumRange + gpsLoc.accuracy) * 2
211 | else false
212 | if (l.suspicious && !tooLarge) { // if it will be too large, always update (effectively blacklists this emitter)
213 | if (DEBUG) Log.d(TAG, "updateLocation($logString) - No update because last observation is suspicious")
214 | return
215 | }
216 |
217 | // Don't update location if there is more than 10 sec difference between last observation
218 | // and gps location, or even less if we are moving really fast compared to the emitter range
219 | // (because we might have moved considerably during this time).
220 | // This can occur e.g. if a WiFi scan takes very long to complete or old scan results are reported
221 | val tDiff = abs(l.elapsedRealtimeNanos - gpsLoc.elapsedRealtimeNanos) * 1e-9
222 | val tDiffMax = if (gpsLoc.hasSpeed() && gpsLoc.speed > 0)
223 | // time we need to move through half the maximum range, but at most 10s
224 | (ourCharacteristics.maximumRange / 2 / gpsLoc.speed).coerceAtMost(10.0)
225 | else 10.0
226 | if (tDiff > tDiffMax) {
227 | if (DEBUG) Log.d(TAG, "updateLocation($logString) - No update because location and observation " +
228 | "differ too much: ${(l.elapsedRealtimeNanos - gpsLoc.elapsedRealtimeNanos)/1e6}ms")
229 | return
230 | }
231 |
232 | // don't update coverage if gps too inaccurate
233 | // except if if emitter would grow too large after updating, in which case we want to blacklist it
234 | if (gpsLoc.accuracy > ourCharacteristics.requiredGpsAccuracy && !tooLarge) {
235 | if (DEBUG) Log.d(TAG, "updateLocation($logString) - No update because location inaccurate. accuracy ${gpsLoc.accuracy}, required ${ourCharacteristics.requiredGpsAccuracy}")
236 | return
237 | }
238 | if (cov == null) {
239 | if (DEBUG) Log.d(TAG, "updateLocation($logString) - Emitter is new.")
240 | coverage = BoundingBox(gpsLoc.latitude, gpsLoc.longitude)
241 | changeStatus(EmitterStatus.STATUS_NEW, "updateLocation($logString) New")
242 | return
243 | }
244 |
245 | // Add the GPS sample to the known bounding box of the emitter.
246 | if (cov.update(gpsLoc.latitude, gpsLoc.longitude)) {
247 | // Bounding box has increased, see if it is now unbelievably large
248 | if (cov.radius > ourCharacteristics.maximumRange)
249 | changeStatus(EmitterStatus.STATUS_BLACKLISTED, "updateLocation($logString) too large radius")
250 | else
251 | changeStatus(EmitterStatus.STATUS_CHANGED, "updateLocation($logString) BBOX update")
252 | }
253 | }
254 |
255 | /**
256 | * RfLocation for backendService. Differs from internal one in that we don't report
257 | * locations that are guarded due to being new or moved.
258 | *
259 | * @return The coverage estimate and further information for our RF emitter or null if
260 | * we don't trust our information.
261 | */
262 | val location: RfLocation?
263 | get() {
264 | // If we have no observation of the emitter we ought not give a
265 | // position estimate based on it.
266 | val observation = lastObservation ?: return null
267 |
268 | if (status == EmitterStatus.STATUS_BLACKLISTED) return null
269 |
270 | // If we don't have a coverage estimate we will get back a null location
271 | val cov = coverage ?: return null
272 |
273 | // If we are unbelievably close to null island, don't report location
274 | if (isNullIsland(cov.center_lat, cov.center_lon)) return null
275 |
276 | // Use time and asu based on most recent observation
277 | return RfLocation(observation.lastUpdateTimeMs, observation.elapsedRealtimeNanos,
278 | cov.center_lat, cov.center_lon, radius, observation.asu, rfIdentification, observation.suspicious)
279 | }
280 |
281 | /**
282 | * As part of our effort to not use mobile emitters in estimating or location
283 | * we blacklist ones that match observed patterns.
284 | *
285 | * @return True if the emitter is blacklisted (should not be used in position computations).
286 | */
287 | private fun isBlacklisted(): Boolean =
288 | if (note.isEmpty()) false
289 | else
290 | when (type) {
291 | WLAN2, WLAN5, WLAN6 -> ssidBlacklisted()
292 | BT -> false // if ever added, there should be a BT blacklist too
293 | else -> false // Not expecting mobile towers to move around.
294 | }
295 |
296 | /**
297 | * Checks the note field (where the SSID is saved) to see if it appears to be
298 | * an AP that is likely to be moving. Typical checks are to see if substrings
299 | * in the SSID match that of cell phone manufacturers or match known patterns
300 | * for public transport (busses, trains, etc.) or in car WLAN defaults.
301 | *
302 | * @return True if emitter should be blacklisted.
303 | */
304 | private fun ssidBlacklisted(): Boolean {
305 | val lc = note.lowercase()
306 |
307 | // split lc into continuous occurrences of a-z
308 | // most 'contains' checks only make sense if the string is a separate word
309 | // this accelerates comparison a lot, at the risk of missing some WiFis
310 | val lcSplit = lc.split(splitRegex).toHashSet()
311 |
312 | // Seen a large number of WiFi networks where the SSID is the last
313 | // three octets of the MAC address. Often in rural areas where the
314 | // only obvious source would be other automobiles. So suspect that
315 | // this is the default setup for a number of vehicle manufactures.
316 | val macSuffix =
317 | id.substring(id.length - 8).lowercase().replace(":", "")
318 |
319 | val blacklisted =
320 | lcSplit.any { blacklistWords.contains(it) }
321 | || blacklistStartsWith.any { lc.startsWith(it) }
322 | || blacklistEndsWith.any { lc.endsWith(it) }
323 | || blacklistEquals.contains(lc)
324 | // a few less simple checks
325 | || lcSplit.contains("moto") && note.startsWith("MOTO") // "MOTO9564" and "MOTO9916" seen
326 | || lcSplit.first() == "audi" // some cars seem to have this AP on-board
327 | || lc == macSuffix // Apparent default SSID name for many cars
328 | // deal with words not achievable with the blacklist sets, checking only if
329 | // lcSplit.contains() (for performance reasons)
330 | || (lcSplit.contains("admin") && lc.contains("admin@ms"))
331 | || (lcSplit.contains("guest") && lc.contains("guest@ms"))
332 | || (lcSplit.contains("contiki") && lc.contains("contiki-wifi")) // transport
333 | || (lcSplit.contains("interakti") && lc.contains("nsb_interakti")) // ???
334 | || (lcSplit.contains("nvram") && lc.contains("nvram warning")) // transport
335 |
336 | if (DEBUG && blacklisted) Log.d(TAG, "blacklistWifi('$logString'): blacklisted")
337 | return blacklisted
338 | }
339 |
340 | /**
341 | * Our status can only make a small set of allowed transitions. Basically a simple
342 | * state machine. To assure our transitions are all legal, this routine is used for
343 | * all changes.
344 | *
345 | * @param newStatus The desired new status (state)
346 | * @param info Logging information for debug purposes
347 | */
348 | private fun changeStatus(newStatus: EmitterStatus, info: String) {
349 | if (newStatus == status) return
350 | when (status) {
351 | EmitterStatus.STATUS_BLACKLISTED -> { }
352 | EmitterStatus.STATUS_CACHED, EmitterStatus.STATUS_CHANGED ->
353 | when (newStatus) {
354 | EmitterStatus.STATUS_BLACKLISTED, EmitterStatus.STATUS_CACHED, EmitterStatus.STATUS_CHANGED ->
355 | status = newStatus
356 | else -> { }
357 | }
358 | EmitterStatus.STATUS_NEW ->
359 | when (newStatus) {
360 | EmitterStatus.STATUS_BLACKLISTED, EmitterStatus.STATUS_CACHED ->
361 | status = newStatus
362 | else -> { }
363 | }
364 | EmitterStatus.STATUS_UNKNOWN ->
365 | when (newStatus) {
366 | EmitterStatus.STATUS_BLACKLISTED, EmitterStatus.STATUS_CACHED, EmitterStatus.STATUS_NEW ->
367 | status = newStatus
368 | else -> { }
369 | }
370 | }
371 | if (DEBUG) Log.d(TAG, "$info: tried switching to $newStatus, result: $status")
372 | return
373 | }
374 | }
375 |
376 | private val DEBUG = BuildConfig.DEBUG
377 |
378 | private const val TAG = "LocalNLP RfEmitter"
379 |
380 | private val splitRegex = "[^a-z]".toRegex() // for splitting SSID into "words"
381 | // use hashSets for fast blacklist*.contains() check
382 | private val blacklistWords = hashSetOf(
383 | "android", "ipad", "iphone", "phone", "motorola", "huawei", "nokia", "redmi", "realme",
384 | "honor", "oppo", "galaxy", "oneplus", // mobile tethering
385 | "mobile", // sounds like name for mobile hotspot
386 | "deinbus", "ecolines", "eurolines", "fernbus", "flixbus", "muenchenlinie",
387 | "postbus", "skanetrafiken", "oresundstag", "regiojet", "hotspotarriva", // transport
388 |
389 | // Per an instructional video on YouTube, recent (2014 and later) Chrysler-Fiat
390 | // vehicles have a SSID of the form "Chrysler uconnect xxxxxx" where xxxxxx
391 | // seems to be a hex digit string (suffix of BSSID?).
392 | "uconnect", // Chrysler built vehicles
393 | "chevy", // "Chevy Cruz 7774" and "Davids Chevy" seen.
394 | "silverado", // GMC Silverado. "Bryces Silverado" seen, maybe move to startsWith?
395 | "myvolvo", // Volvo in car WiFi, maybe move to startsWith?
396 | "bmw", // examples: BMW98303 CarPlay, My BMW Hotspot 8303, DIRECT-BMW 67727
397 | "skoda", // My Skoda 3358, Skoda_WLAN_5790
398 | "seat", // My SEAT 741, SEAT_WLAN
399 | "vw", // VW WLAN 9266, VW_WLAN, My VW 4025
400 | )
401 | private val blacklistEquals = hashSetOf(
402 | "amtrak", "amtrakconnect", "cdwifi", "megabus", "westlan","wifi in de trein",
403 | "svciob", "oebb", "oebb-postbus", "dpmbfree", "telekom_ice", "db ic bus",
404 | "gkbgast", "mavstart-wifi", "wifionice", "wifi@db", "crosscountrywifi",
405 | "gwr wifi", "thalysnet", "_sncf_wifi_inoui", "_sncf_wifi_intercities",
406 | "normandietrainconnecte", "keolis nederland", "ouifi", "raillan", "vorwlan",
407 | "zssk wifi", "wifi zssk", "mavstart-wifi", "raaberbahn", "hotspot ic", "vmobil" // transport
408 | )
409 | // and arrays if we just want to iterate
410 | private val blacklistStartsWith = arrayOf(
411 | "moto ", "lg aristo", "androidap", "vivo ", "mi ", // mobile tethering
412 | "cellspot", // T-Mobile US portable cell based WiFi
413 | "verizon", // Verizon mobile hotspot
414 |
415 | // Per some instructional videos on YouTube, recent (2015 and later)
416 | // General Motors built vehicles come with a default WiFi SSID of the
417 | // form "WiFi Hotspot 1234" where the 1234 is different for each car.
418 | "wifi hotspot ", // Default GM vehicle WiFi name
419 |
420 | // Per instructional video on YouTube, Mercedes cars have and SSID of
421 | // "MB WLAN nnnnn" where nnnnn is a 5 digit number, same for MB Hostspot and direct-mb hotspot
422 | "mb wlan ", "mb hotspot", "direct-mb hotspot",
423 | "westbahn ", "buswifi", "coachamerica", "disneylandresortexpress",
424 | "taxilinq", "transitwirelesswifi", // transport, maybe move some to words?
425 | "yicarcam", // Dashcam WiFi
426 | )
427 | private val blacklistEndsWith = arrayOf(
428 | "corvette", // Chevy Corvette. "TS Corvette" seen.
429 |
430 | // General Motors built vehicles SSID can be changed but the recommended SSID to
431 | // change to is of the form "first_name vehicle_model" (e.g. "Bryces Silverado").
432 | "truck", // "Morgans Truck" and "Wally Truck" seen
433 | "suburban", // Chevy/GMC Suburban. "Laura Suburban" seen
434 | "terrain", // GMC Terrain. "Nelson Terrain" seen
435 | "sierra", // GMC pickup. "dees sierra" seen
436 | "gmc wifi", // General Motors
437 | )
438 |
439 | enum class EmitterStatus {
440 | STATUS_UNKNOWN, // Newly discovered emitter, no data for it at all
441 | STATUS_NEW, // Not in database but we've got location data for it
442 | STATUS_CHANGED, // In database but something has changed
443 | STATUS_CACHED, // In database no changes pending
444 | STATUS_BLACKLISTED // Has been blacklisted
445 | }
446 |
447 | // most recent location information about the emitter
448 | data class RfLocation(
449 | /** timestamp of most recent observation, like System.currentTimeMillis() */
450 | val time: Long,
451 | /** elapsedRealtimeNanos of most recent observation */
452 | val elapsedRealtimeNanos: Long,
453 | val lat: Double,
454 | val lon: Double,
455 | /** emitter radius, may be 0 */
456 | val radius: Double,
457 | /** asu of most recent observation */
458 | val asu: Int,
459 | val id: RfIdentification,
460 | /** whether we suspect the most recent observation might not be entirely correct */
461 | val suspicious: Boolean,
462 | ) {
463 | /** emitter radius, but at least minimumRange for this EmitterType */
464 | val accuracyEstimate: Double = radius.coerceAtLeast(id.rfType.getRfCharacteristics().minimumRange)
465 | }
466 |
--------------------------------------------------------------------------------