├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── debug │ └── res │ │ └── values │ │ └── appName.xml │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── org │ │ └── fitchfamily │ │ └── android │ │ └── dejavu │ │ ├── BackendService.kt │ │ ├── BoundingBox.kt │ │ ├── Cache.kt │ │ ├── Database.kt │ │ ├── GpsMonitor.kt │ │ ├── HandleGeoUriActivity.kt │ │ ├── Kalman.java │ │ ├── Kalman1Dim.java │ │ ├── Observation.kt │ │ ├── RfCharacteristics.kt │ │ ├── RfEmitter.kt │ │ ├── RfIdentification.kt │ │ ├── SettingsActivity.kt │ │ └── Util.kt │ └── res │ ├── drawable │ ├── ic_launcher_background.xml │ ├── ic_launcher_foreground.xml │ └── ic_notification.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── values-de │ └── strings.xml │ ├── values-eo │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-pl │ └── strings.xml │ ├── values-ru │ └── strings.xml │ ├── values-uk │ └── strings.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values │ ├── appName.xml │ ├── arrays.xml │ ├── colors.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── preferences.xml ├── build.gradle ├── fastlane └── metadata │ └── android │ ├── de │ └── short_description.txt │ └── en-US │ ├── changelogs │ ├── 15.txt │ ├── 16.txt │ ├── 17.txt │ ├── 18.txt │ ├── 19.txt │ ├── 20.txt │ ├── 21.txt │ ├── 23.txt │ ├── 24.txt │ ├── 25.txt │ ├── 26.txt │ ├── 27.txt │ ├── 28.txt │ ├── 29.txt │ ├── 30.txt │ ├── 31.txt │ ├── 32.txt │ ├── 33.txt │ ├── 34.txt │ ├── 35.txt │ ├── 36.txt │ ├── 38.txt │ ├── 39.txt │ └── 40.txt │ ├── full_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ └── 1.png │ ├── short_description.txt │ └── title.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── res ├── Geolocation_-_The_Noun_Project.svg ├── Geolocation_-_The_Noun_Project_mod.svg └── authors.txt └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | tags 2 | .DS_Store 3 | local.properties 4 | gen/ 5 | .idea/ 6 | out/ 7 | *.iml 8 | build/ 9 | *.apk 10 | .gradle/ 11 | user.gradle 12 | local.properties 13 | app/release 14 | -------------------------------------------------------------------------------- /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.14] - 2025-01-22 18 | ### Changed 19 | - Added settings to launcher to allow data export without microG support 20 | - Upgrade dependencies 21 | 22 | ## [1.2.13] - 2024-12-22 23 | ### Changed 24 | - Fix wrong check breaking imports 25 | - Extend blacklist 26 | - Upgrade dependencies 27 | 28 | ## [1.2.12] - 2024-04-22 29 | ### Changed 30 | - Extend blacklist 31 | - Avoid crashes due to invalid emitter type 32 | - Upgrade dependencies 33 | 34 | ## [1.2.11] - 2023-08-20 35 | ### Changed 36 | - Import MLS / OpenCelliD lists without header 37 | 38 | ## [1.2.10] - 2023-05-22 39 | ### Changed 40 | - Extended blacklist (thanks to Sorunome) 41 | - Avoid searching nearby WiFis if GPS accuracy isn't good enough 42 | 43 | ## [1.2.9] - 2023-05-03 44 | ### Added 45 | - Handle geo uris: allows adding emitters as if a GPS location was received at the indicated location 46 | 47 | ### Changed 48 | - Improved blacklisting of unbelievably large emitters 49 | 50 | ## [1.2.8] - 2023-04-27 51 | ### Changed 52 | - Fix potential import / export issue 53 | 54 | ## [1.2.7] - 2023-04-25 55 | ### Changed 56 | - Crash fix 57 | - Small UI changes when viewing nearby emitters and emitter details 58 | 59 | ## [1.2.6] - 2023-04-19 60 | ### Changed 61 | - Notification text for active mode now contains name of emitter that triggered the scan 62 | - Keep screen on during import / export operations 63 | 64 | ## [1.2.5] - 2023-02-10 65 | ### Changed 66 | - Fix MLS import not working without MCC filter 67 | - Support placeholder for simplified MCC filtering 68 | - Fix bugs when importing files 69 | - Clarify that OpenCelliD files can be used too, as the format is same as MLS 70 | - Switch from Light theme to DayNight theme 71 | 72 | ## [1.2.4] - 2023-01-30 73 | ### Changed 74 | - Fix not (properly) asking for background location, resulting in no location permissions being asked on Android 11+ 75 | - Update microG NLP API and other dependencies 76 | 77 | ## [1.2.3] - 2022-12-16 78 | ### Changed 79 | - Extend blacklist 80 | - Allow more aggressive active mode settings: fill the database better, but may increase battery use 81 | 82 | ## [1.2.2] - 2022-12-11 83 | ### Changed 84 | - Different application id for debug builds 85 | - Fix mobile emitters not being stored on some devices 86 | - Improve storing/updating emitters, especially when using active mode 87 | - Extend blacklist 88 | 89 | ## [1.2.2.beta.1] - 2022-10-11 90 | ### Added 91 | - Support for 5G and TDSCDMA cells 92 | 93 | ### Changed 94 | - Fix crashes 95 | - Upgrade to API 33 96 | 97 | ## [1.2.1] - 2022-10-05 98 | ### Added 99 | - Manually blacklist emitter when showing nearby emitters. 100 | - Active mode: enable GPS when emitters are found, but none has a known location (disabled by default). 101 | 102 | ### Changed 103 | - Update blacklist. 104 | 105 | ## [1.2.0] - 2022-09-25 106 | ### Changed 107 | - Update blacklist. 108 | 109 | ## [1.2.0-beta.4] - 2022-09-13 110 | ### Changed 111 | - Fix database import from content URI. Now import should work on all devices. 112 | 113 | ## [1.2.0-beta.3] - 2022-09-08 114 | ### Changed 115 | - fix crash when showing nearby emitters 116 | - slightly less ugly buttons when showing nearby emitters 117 | 118 | ## [1.2.0-beta.2] - 2022-09-07 119 | ### Added 120 | - Progress bars for import and export 121 | 122 | ### Changed 123 | - fix MLS import for LTE cells 124 | - fix import of files exported with Local NLP Backend 125 | - faster import 126 | - reworked database code 127 | - upgrade dependencies 128 | - prepare for API upgrade (will remove deprecated getNeighboringCellInfo function, which may be used by some old devices) 129 | 130 | ## [1.2.0-beta] - 2022-09-07 131 | ### Added 132 | - 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. 133 | - Blacklist emitters with suspiciously high radius, as they may actually be mobile hotspots. 134 | - Don't use outdated WiFi scan results if scan is not successful. This helps especially against WiFi throttling introduced in Android 9. 135 | - Consider signal strength when estimating accuracy. 136 | 137 | ### Changed 138 | - New app and package names. 139 | - New icon (modified from: https://thenounproject.com/icon/38241). 140 | - Some small bug fixes. 141 | - Update and actually use the WiFi blacklist. 142 | - Faster, but less exact distance calculations. For the used distances up to 100 km, the differences are negligible. 143 | - Ignore cell emitters with invalid LAC. 144 | - 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. 145 | - Consider that LTE and 3G cells are usually smaller than GSM cells. 146 | - 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. 147 | - Adjusted how position and accuracy are determined. 148 | 149 | ### Removed 150 | - 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. 151 | - 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. 152 | 153 | ## [1.1.12] - 2019-08-12 154 | 155 | ### Changed 156 | - Update gradle build environment. 157 | - Add debug logging for detection of 5G WiFi/WLAN networds. 158 | - Add some Czech, Austrian and Dutch transport WLANs to ignore list. 159 | 160 | ## [1.1.11] - 2019-04-21 161 | ### Added 162 | - Add Esperanto and Polish translations 163 | 164 | ### Changed 165 | - Update gradle build environment 166 | - Revise list of WLAN/WiFi SSIDs to ignore 167 | 168 | ## [1.1.10] - 2018-12-18 169 | ### Added 170 | - Ignore WLANs on trains and buses of transit agencies in southwest Sweden. Thanks to lbschenkel 171 | - Ignore Austrian train WLANs. Thanks to akallabeth 172 | 173 | ### Changed 174 | - Update Gradle build environment 175 | - Revise checks for locations near lat/lon of 0,0 176 | 177 | ## [1.1.9] - 2018-09-06 178 | ### Added 179 | - Chinese translation (thanks to @Crystal-RainSlide) 180 | - Protect against external GPS giving locations near 0.0/0.0 181 | 182 | ## [1.1.8] - 2018-06-21 183 | ### Added 184 | - Initial support for 5 GHz WLAN RF characteristics being different than 2.4 GHz WLAN. Note: 5GHz WLAN not tested on a real device. 185 | 186 | ### Changed 187 | - Fix timing related crash on start up/shut down 188 | - Revisions to better support external GPS with faster report rates. 189 | - Revise database to allow same identifier on multiple emitter types. 190 | - Updated build tools and target API version 191 | 192 | ## [1.1.7] - 2018-06-18 193 | ### Changed 194 | - Fix crash on empty set of seen emitters. 195 | - Fix some Lint identified items. 196 | 197 | ## [1.1.6] - 2018-06-17 198 | ### Added 199 | - Add Ukrainian translation 200 | 201 | ### Changed 202 | - Build specification to reduce size of released application. 203 | - Update build environment 204 | 205 | ## [1.1.5] - 2018-03-19 206 | ### Added 207 | - Russian Translation. Thanks to @bboa 208 | 209 | ## [1.1.4] - 2018-03-12 210 | ### Added 211 | - German Translation. Thanks to @levush 212 | 213 | ## [1.1.3] - 2018-02-27 214 | 215 | ### Changed 216 | - Protect against accessing null object. 217 | 218 | ## [1.1.2] - 2018-02-11 219 | 220 | ### Changed 221 | - Fix typo in Polish strings. Thanks to @verdulo 222 | 223 | ## [1.1.1] - 2018-01-30 224 | ### Changed 225 | - Refactor/clean up logic flow and position computation. 226 | 227 | ## [1.1.0] - 2018-01-25 228 | ### Changed 229 | - 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. 230 | 231 | ## [1.0.8] - 2018-01-12 232 | ### Added 233 | - Polish Translation. Thanks to @verdulo 234 | 235 | ## [1.0.7] - 2018.01.05 236 | ### Changed 237 | - Avoid crash on start up if database is not available when first RF emitter is processed. 238 | 239 | ## [1.0.6] - 2017-12-28 240 | ### Added 241 | - French translation. Thanks to @Massedil. 242 | 243 | ## [1.0.5] - 2017-12-24 244 | ### Added 245 | - Partial support for CDMA and WCDMA towers when using getAllCellInfo() API. 246 | 247 | ### Changed 248 | - Check for unknown values in fields in the cell IDs returned by getAllCellInfo(); 249 | 250 | ## [1.0.4] - 2017-12-18 251 | ### Changed 252 | - Add more checks for permissions not granted to avoid locking up phone. 253 | 254 | ## [1.0.3] 255 | ### Changed 256 | - Correct blacklist logic 257 | 258 | ## [1.0.2] 259 | ### Changed 260 | - Correct versionCode and versionName in gradle.build 261 | 262 | ## [1.0.1] 263 | ### Changed 264 | - Corrected package ID in manifest 265 | 266 | ## [1.0.0] 267 | ### Added 268 | - Initial Release 269 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainer. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /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 | [Get it on F-Droid](https://f-droid.org/packages/helium314.localbackend/) 17 | [Download APK from GitHub](https://github.com/Helium314/Local-NLP-Backend/releases/latest) 18 | 19 | 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. 20 | 21 | 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*. 22 | 23 | How to use 24 | ========== 25 | 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. 26 | 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. 27 | Note that the local database needs to be filled either using GPS or by importing data, before Local NLP Backend can provide locations! 28 | 29 | 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). 30 | 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. 31 | 32 | 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. 33 | 34 | Potential improvements not yet implemented 35 | ====================== 36 | Local NLP Backend works mostly fine as it is, but there are some areas where it could be improved: 37 | * 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. 38 | * 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. 39 | * 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. 40 | * determination of position from found emitters is just working "good enough", but not great. A different approach might yield better results. 41 | * 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. 42 | 43 | Requirements on phone 44 | ===================== 45 | This is a plug-in for [microG](https://microg.org/) (UnifiedNlp or GmsCore). 46 | 47 | Setup on phone 48 | ============== 49 | 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. 50 | 51 | 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. 52 | 53 | 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. 54 | 55 | Collecting RF Emitter Data 56 | ====================== 57 | 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. 58 | Alternatively you can enable active mode in the settings available via microG backend configuration. 59 | 60 | What is stored in the database 61 | ------------------------------ 62 | For each RF emitter detected an estimate of its coverage area (center and extents) is saved. 63 | 64 | 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"). 65 | 66 | Clearing the database 67 | --------------------- 68 | 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. 69 | 70 | Permissions Required 71 | ==================== 72 | |Permission|Use| 73 | |:----------|:---| 74 | ACCESS_COARSE_LOCATION|Allows backend to determine which cell towers your phone detects. 75 | ACCESS_FINE_LOCATION|Allows backend to determine which WiFis your phone detect and monitor position reports from the GPS. 76 | ACCESS_BACKGROUND_LOCATION|Necessary on Android 10 and higher, as the backend only runs in foreground when using active mode. 77 | CHANGE_WIFI_STATE|Allows backend to scan for nearby WiFis. 78 | ACCESS_WIFI_STATE|Allows backend to access WiFi scan results. 79 | FOREGROUND_SERVICE|Needed so GPS can be used in active mode. 80 | 81 | Some permissions may not be necessary, this heavily depends on the [Android version](https://developer.android.com/guide/topics/connectivity/wifi-scan). 82 | 83 | Changes 84 | ======= 85 | [Revision history is kept in a separate change log.](CHANGELOG.md) 86 | 87 | Credits 88 | ======= 89 | The Kalman filter used in this backend is based on [work by @villoren](https://github.com/villoren/KalmanLocationManager.git). 90 | 91 | Most of this project is adjusted from [Déjà Vu](https://github.com/n76/DejaVu) 92 | 93 | License 94 | ======= 95 | 96 | Most of this project is licensed by GNU GPL. The Kalman filter code retains its original MIT license. 97 | 98 | Icon 99 | ---- 100 | The icon for this project is derived from: 101 | 102 | [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). 103 | 104 | GNU General Public License 105 | -------------------------- 106 | Copyright (C) 2017-18 Tod Fitch 107 | Copyright (C) 2022-23 Helium314 108 | 109 | 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. 110 | 111 | 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. 112 | 113 | You should have received a copy of the GNU General Public License 114 | 115 | MIT License 116 | ----------- 117 | Permission is hereby granted, free of charge, to any person obtaining a copy 118 | of this software and associated documentation files (the "Software"), to deal 119 | in the Software without restriction, including without limitation the rights 120 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 121 | copies of the Software, and to permit persons to whom the Software is 122 | furnished to do so, subject to the following conditions: 123 | 124 | The above copyright notice and this permission notice shall be included in all 125 | copies or substantial portions of the Software. 126 | 127 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 128 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 129 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 130 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 131 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 132 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 133 | SOFTWARE. 134 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion 34 6 | defaultConfig { 7 | applicationId "helium314.localbackend" 8 | minSdkVersion 18 9 | targetSdkVersion 34 10 | versionCode 42 11 | versionName "1.2.14" 12 | } 13 | 14 | buildTypes { 15 | release { 16 | shrinkResources true 17 | minifyEnabled true 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | debug { 21 | applicationIdSuffix = ".debug" 22 | } 23 | } 24 | 25 | buildFeatures { 26 | buildConfig true 27 | } 28 | 29 | lintOptions { 30 | disable 'MissingTranslation' 31 | } 32 | 33 | namespace "org.fitchfamily.android.dejavu" 34 | archivesBaseName = "local-nlp-backend_" + defaultConfig.versionName 35 | 36 | compileOptions { 37 | sourceCompatibility JavaVersion.VERSION_17 38 | targetCompatibility JavaVersion.VERSION_17 39 | } 40 | 41 | kotlinOptions { 42 | jvmTarget = JavaVersion.VERSION_17.toString() 43 | } 44 | } 45 | 46 | dependencies { 47 | implementation 'androidx.appcompat:appcompat:1.6.1' // can't upgrade to 1.7.0 because this requires minSdkVersion 21 48 | implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' 49 | implementation 'org.microg.nlp:api:2.0-alpha10' 50 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1' 51 | } 52 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can edit the include path and order by changing the proguardFiles 3 | # directive in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # Add any project specific keep options here: 9 | 10 | # Uncomment this to preserve the line number information for 11 | # debugging stack traces. 12 | #-keepattributes SourceFile,LineNumberTable 13 | 14 | # If you keep the line number information, uncomment this to 15 | # hide the original source file name. 16 | #-renamesourcefileattribute SourceFile 17 | 18 | -dontobfuscate 19 | -------------------------------------------------------------------------------- /app/src/debug/res/values/appName.xml: -------------------------------------------------------------------------------- 1 | 2 | Local NLP Backend Debug 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 17 | 18 | 23 | 24 | 25 | 26 | 27 | 30 | 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/java/org/fitchfamily/android/dejavu/BoundingBox.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 java.lang.Math.toRadians 25 | import kotlin.math.cos 26 | import kotlin.math.sqrt 27 | 28 | /** 29 | * Created by tfitch on 9/28/17. 30 | * modified by helium314 in 2022 31 | */ 32 | class BoundingBox private constructor() { 33 | var center_lat: Double = 0.0 34 | private set 35 | var center_lon: Double = 0.0 36 | private set 37 | var radius_ns: Double = 0.0 38 | private set 39 | var radius_ew: Double = 0.0 40 | private set 41 | 42 | var north = -91.0 // Impossibly south 43 | private set 44 | var south = 91.0 // Impossibly north 45 | private set 46 | var east = -181.0 // Impossibly west 47 | private set 48 | var west = 181.0 // Impossibly east 49 | private set 50 | var radius = 0.0 51 | private set 52 | 53 | constructor(info: EmitterInfo) : this(info.latitude, info.longitude, info.radius_ns, info.radius_ew) 54 | 55 | constructor(lat: Double, lon: Double) : this() { 56 | update(lat, lon) 57 | } 58 | 59 | constructor(lat: Double, lon: Double, r_ns: Double, r_ew: Double) : this() { 60 | if (r_ns < 0 || r_ew < 0) throw IllegalArgumentException("radii cannot be < 0") 61 | center_lat = lat 62 | center_lon = lon 63 | radius_ns = r_ns 64 | radius_ew = r_ew 65 | radius = sqrt(radius_ns * radius_ns + radius_ew * radius_ew) 66 | 67 | north = center_lat + radius_ns * METER_TO_DEG 68 | south = center_lat - radius_ns * METER_TO_DEG 69 | val cosLat = cos(toRadians(center_lat)).coerceAtLeast(MIN_COS) 70 | east = center_lon + radius_ew * METER_TO_DEG / cosLat 71 | west = center_lon - radius_ew * METER_TO_DEG / cosLat 72 | } 73 | 74 | /** 75 | * Update the bounding box to include a point at the specified lat/lon 76 | * @param lat The latitude to be included in the bounding box 77 | * @param lon The longitude to be included in the bounding box 78 | * @return whether coverage has changed 79 | */ 80 | fun update(lat: Double, lon: Double): Boolean { 81 | var updated = false 82 | if (lat > north) { 83 | north = lat 84 | updated = true 85 | } 86 | if (lat < south) { 87 | south = lat 88 | updated = true 89 | } 90 | if (lon > east) { 91 | east = lon 92 | updated = true 93 | } 94 | if (lon < west) { 95 | west = lon 96 | updated = true 97 | } 98 | if (updated) { 99 | center_lat = (north + south) / 2.0 100 | center_lon = (east + west) / 2.0 101 | radius_ns = ((north - center_lat) * DEG_TO_METER) 102 | val cosLat = cos(toRadians(center_lat)).coerceAtLeast(MIN_COS) 103 | radius_ew = ((east - center_lon) * DEG_TO_METER * cosLat) 104 | radius = sqrt(radius_ns * radius_ns + radius_ew * radius_ew) 105 | } 106 | return updated 107 | } 108 | 109 | override fun toString(): String { 110 | return "($north,$west,$south,$east,$center_lat,$center_lon,$radius_ns,$radius_ew,$radius)" 111 | } 112 | 113 | fun contains(location: Location): Boolean = 114 | north > location.latitude && south < location.latitude 115 | && east > location.longitude && west < location.longitude 116 | 117 | override fun equals(other: Any?): Boolean { 118 | if (this === other) return true 119 | if (other !is BoundingBox) return false 120 | return center_lat == other.center_lat && center_lon == other.center_lon 121 | && radius_ns == other.radius_ns && radius_ew == other.radius_ew 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /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/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/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/HandleGeoUriActivity.kt: -------------------------------------------------------------------------------- 1 | package org.fitchfamily.android.dejavu 2 | 3 | import android.app.Activity 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AlertDialog 7 | 8 | /** 9 | * Activity purely for handling geo uri intents. If the intent is valid and the user confirms 10 | * they want the location added, [BackendService.geoUriLocationProvided] is called. 11 | * The activity is always finished when handling is done or intent invalid. 12 | */ 13 | class HandleGeoUriActivity: Activity() { 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | if (!handleGeoUri()) finish() // maybe show toast? 17 | } 18 | 19 | // returns whether dialog is shown, so activity can be finished if not shown 20 | private fun handleGeoUri(): Boolean { 21 | if (BackendService.instance == null) return false 22 | val data = intent.data ?: return false 23 | if (data.scheme != "geo") return false 24 | 25 | // taken from StreetComplete (GeoUri.kt) 26 | val geoUriRegex = Regex("(-?[0-9]*\\.?[0-9]+),(-?[0-9]*\\.?[0-9]+).*?(?:\\?z=([0-9]*\\.?[0-9]+))?") 27 | val match = geoUriRegex.matchEntire(data.schemeSpecificPart) ?: return false 28 | val latitude = match.groupValues[1].toDoubleOrNull() ?: return false 29 | if (latitude < -90 || latitude > +90) return false 30 | val longitude = match.groupValues[2].toDoubleOrNull() ?: return false 31 | if (longitude < -180 || longitude > +180) return false 32 | 33 | AlertDialog.Builder(this) 34 | .setTitle(R.string.app_name) 35 | .setMessage(getString(R.string.handle_geo_uri_message, latitude, longitude)) 36 | .setNegativeButton(android.R.string.cancel) { _, _ -> finish() } 37 | .setOnCancelListener { finish() } 38 | .setPositiveButton(android.R.string.ok) { _, _ -> 39 | BackendService.geoUriLocationProvided(latitude, longitude) 40 | finish() 41 | } 42 | .show() 43 | return true 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/Observation.kt: -------------------------------------------------------------------------------- 1 | package org.fitchfamily.android.dejavu 2 | 3 | import org.fitchfamily.android.dejavu.BackendService.Companion.getCorrectedAsu 4 | 5 | /* 6 | * Local NLP Backend / DejaVu - A location provider backend for microG/UnifiedNlp 7 | * 8 | * Copyright (C) 2017 Tod Fitch 9 | * Copyright (C) 2022 Helium314 10 | * 11 | * This program is Free Software: you can redistribute it and/or modify 12 | * it under the terms of the GNU General Public License as 13 | * published by the Free Software Foundation, either version 3 of the 14 | * License, or (at your option) any later version. 15 | * 16 | * This program is distributed in the hope that it will be useful, 17 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | * GNU General Public License for more details. 20 | * 21 | * You should have received a copy of the GNU General Public License 22 | * along with this program. If not, see . 23 | */ 24 | 25 | /** 26 | * Created by tfitch on 10/5/17. 27 | * modified by helium314 in 2022 28 | */ 29 | /** 30 | * A single observation made of a RF emitter. 31 | * 32 | * Used to convey all the information we have collected in the foreground about 33 | * a RF emitter we have seen to the background thread that actually does the 34 | * heavy lifting. 35 | * 36 | * It contains an identifier for the RF emitter (type and id), the received signal 37 | * level and optionally a note about about the emitter. 38 | */ 39 | data class Observation( 40 | val identification: RfIdentification, 41 | var asu: Int = MINIMUM_ASU, 42 | val elapsedRealtimeNanos: Long, 43 | val note: String = "", 44 | val suspicious: Boolean = false, // means that we don't trust the device that observation is correct 45 | ) { 46 | internal constructor(id: String, type: EmitterType, asu: Int, realtimeNanos: Long) : this(RfIdentification(id, type), asu, realtimeNanos) 47 | 48 | init { 49 | asu = identification.rfType 50 | .getCorrectedAsu(asu.coerceAtLeast(MINIMUM_ASU).coerceAtMost(MAXIMUM_ASU)) 51 | } 52 | 53 | val lastUpdateTimeMs = System.currentTimeMillis() 54 | 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/org/fitchfamily/android/dejavu/RfCharacteristics.kt: -------------------------------------------------------------------------------- 1 | package org.fitchfamily.android.dejavu 2 | 3 | import java.util.* 4 | 5 | /* 6 | * Local NLP Backend / DejaVu - A location provider backend for microG/UnifiedNlp 7 | * 8 | * Copyright (C) 2017 Tod Fitch 9 | * Copyright (C) 2022 Helium314 10 | * 11 | * This program is Free Software: you can redistribute it and/or modify 12 | * it under the terms of the GNU General Public License as 13 | * published by the Free Software Foundation, either version 3 of the 14 | * License, or (at your option) any later version. 15 | * 16 | * This program is distributed in the hope that it will be useful, 17 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | * GNU General Public License for more details. 20 | * 21 | * You should have received a copy of the GNU General Public License 22 | * along with this program. If not, see . 23 | */ 24 | 25 | // moved from RfEmitter to separate file 26 | 27 | class RfCharacteristics ( 28 | val requiredGpsAccuracy: Float, // Required accuracy for updating emitter coverage (should be less than half of minimumRange, as GPS is frequently off by more then the accuracy) 29 | val minimumRange: Double, // Minimum believable coverage radius in meters 30 | val maximumRange: Double, // Maximum believable coverage radius in meters 31 | val minCount: Int // Minimum number of emitters before we can estimate location 32 | ) 33 | 34 | enum class EmitterType { 35 | INVALID, 36 | WLAN2, 37 | WLAN5, 38 | WLAN6, 39 | BT, 40 | GSM, 41 | CDMA, 42 | WCDMA, 43 | TDSCDMA, 44 | LTE, 45 | NR, 46 | NR_FR2, 47 | } 48 | 49 | private const val METERS: Float = 1.0f 50 | private const val KM = METERS * 1000 51 | 52 | val shortRangeEmitterTypes: Set = 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/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 | var newStatus = status 164 | when (status) { 165 | EmitterStatus.STATUS_UNKNOWN -> { } 166 | EmitterStatus.STATUS_BLACKLISTED -> 167 | // If our coverage value is not null it implies that we exist in the 168 | // database as "normal" emitter. If so we ought to either remove the entry (for 169 | // blacklisted SSIDs) or set invalid radius (for too large coverage). 170 | if (coverage != null) { 171 | if (isBlacklisted()) { 172 | db.drop(this) 173 | if (DEBUG) Log.d(TAG, "sync('$logString') - Blacklisted dropping from database.") 174 | } else { 175 | db.setInvalid(this) 176 | if (DEBUG) Log.d(TAG, "sync('$logString') - Blacklisted setting to invalid, radius too large: $radius, $radiusEW, $radiusNS.") 177 | } 178 | coverage = null 179 | } 180 | EmitterStatus.STATUS_NEW -> { 181 | // Not in database, we have location. Add to database 182 | db.insert(this) 183 | newStatus = EmitterStatus.STATUS_CACHED 184 | } 185 | EmitterStatus.STATUS_CHANGED -> { 186 | // In database but we have changes 187 | db.update(this) 188 | newStatus = EmitterStatus.STATUS_CACHED 189 | } 190 | EmitterStatus.STATUS_CACHED -> { } 191 | } 192 | changeStatus(newStatus, "sync('$logString')") 193 | } 194 | 195 | val logString get() = if (DEBUG) "RF Emitter: Type=$type, ID='$id', Note='$note'" else "" 196 | 197 | /** 198 | * Update our estimate of the coverage and location of the emitter based on a 199 | * position report from the GPS system. 200 | * 201 | * @param gpsLoc A position report from a trusted (non RF emitter) source 202 | */ 203 | fun updateLocation(gpsLoc: Location) { 204 | if (status == EmitterStatus.STATUS_BLACKLISTED) return 205 | val l = lastObservation ?: return // can't update emitters we haven't seen 206 | val cov = coverage 207 | // determine whether emitter will grow to unrealistic size if updated 208 | val tooLarge = if (cov != null) approximateDistance(gpsLoc.latitude, gpsLoc.longitude, cov.center_lat, cov.center_lon) > (type.getRfCharacteristics().maximumRange + gpsLoc.accuracy) * 2 209 | else false 210 | if (l.suspicious && !tooLarge) { // if it will be too large, always update (effectively blacklists this emitter) 211 | if (DEBUG) Log.d(TAG, "updateLocation($logString) - No update because last observation is suspicious") 212 | return 213 | } 214 | 215 | // Don't update location if there is more than 10 sec difference between last observation 216 | // and gps location, or even less if we are moving really fast compared to the emitter range 217 | // (because we might have moved considerably during this time). 218 | // This can occur e.g. if a WiFi scan takes very long to complete or old scan results are reported 219 | val tDiff = abs(l.elapsedRealtimeNanos - gpsLoc.elapsedRealtimeNanos) * 1e-9 220 | val tDiffMax = if (gpsLoc.hasSpeed() && gpsLoc.speed > 0) 221 | // time we need to move through half the maximum range, but at most 10s 222 | (ourCharacteristics.maximumRange / 2 / gpsLoc.speed).coerceAtMost(10.0) 223 | else 10.0 224 | if (tDiff > tDiffMax) { 225 | if (DEBUG) Log.d(TAG, "updateLocation($logString) - No update because location and observation " + 226 | "differ too much: ${(l.elapsedRealtimeNanos - gpsLoc.elapsedRealtimeNanos)/1e6}ms") 227 | return 228 | } 229 | 230 | // don't update coverage if gps too inaccurate 231 | // except if if emitter would grow too large after updating, in which case we want to blacklist it 232 | if (gpsLoc.accuracy > ourCharacteristics.requiredGpsAccuracy && !tooLarge) { 233 | if (DEBUG) Log.d(TAG, "updateLocation($logString) - No update because location inaccurate. accuracy ${gpsLoc.accuracy}, required ${ourCharacteristics.requiredGpsAccuracy}") 234 | return 235 | } 236 | if (cov == null) { 237 | if (DEBUG) Log.d(TAG, "updateLocation($logString) - Emitter is new.") 238 | coverage = BoundingBox(gpsLoc.latitude, gpsLoc.longitude) 239 | changeStatus(EmitterStatus.STATUS_NEW, "updateLocation($logString) New") 240 | return 241 | } 242 | 243 | // Add the GPS sample to the known bounding box of the emitter. 244 | if (cov.update(gpsLoc.latitude, gpsLoc.longitude)) { 245 | // Bounding box has increased, see if it is now unbelievably large 246 | if (cov.radius > ourCharacteristics.maximumRange) 247 | changeStatus(EmitterStatus.STATUS_BLACKLISTED, "updateLocation($logString) too large radius") 248 | else 249 | changeStatus(EmitterStatus.STATUS_CHANGED, "updateLocation($logString) BBOX update") 250 | } 251 | } 252 | 253 | /** 254 | * RfLocation for backendService. Differs from internal one in that we don't report 255 | * locations that are guarded due to being new or moved. 256 | * 257 | * @return The coverage estimate and further information for our RF emitter or null if 258 | * we don't trust our information. 259 | */ 260 | val location: RfLocation? 261 | get() { 262 | // If we have no observation of the emitter we ought not give a 263 | // position estimate based on it. 264 | val observation = lastObservation ?: return null 265 | 266 | if (status == EmitterStatus.STATUS_BLACKLISTED) return null 267 | 268 | // If we don't have a coverage estimate we will get back a null location 269 | val cov = coverage ?: return null 270 | 271 | // If we are unbelievably close to null island, don't report location 272 | if (!notNullIsland(cov.center_lat, cov.center_lon)) return null 273 | 274 | // Use time and asu based on most recent observation 275 | return RfLocation(observation.lastUpdateTimeMs, observation.elapsedRealtimeNanos, 276 | cov.center_lat, cov.center_lon, radius, observation.asu, rfIdentification, observation.suspicious) 277 | } 278 | 279 | /** 280 | * As part of our effort to not use mobile emitters in estimating or location 281 | * we blacklist ones that match observed patterns. 282 | * 283 | * @return True if the emitter is blacklisted (should not be used in position computations). 284 | */ 285 | private fun isBlacklisted(): Boolean = 286 | if (note.isEmpty()) false 287 | else 288 | when (type) { 289 | WLAN2, WLAN5, WLAN6 -> ssidBlacklisted() 290 | BT -> false // if ever added, there should be a BT blacklist too 291 | else -> false // Not expecting mobile towers to move around. 292 | } 293 | 294 | /** 295 | * Checks the note field (where the SSID is saved) to see if it appears to be 296 | * an AP that is likely to be moving. Typical checks are to see if substrings 297 | * in the SSID match that of cell phone manufacturers or match known patterns 298 | * for public transport (busses, trains, etc.) or in car WLAN defaults. 299 | * 300 | * @return True if emitter should be blacklisted. 301 | */ 302 | private fun ssidBlacklisted(): Boolean { 303 | val lc = note.lowercase() 304 | 305 | // split lc into continuous occurrences of a-z 306 | // most 'contains' checks only make sense if the string is a separate word 307 | // this accelerates comparison a lot, at the risk of missing some WiFis 308 | val lcSplit = lc.split(splitRegex).toHashSet() 309 | 310 | // Seen a large number of WiFi networks where the SSID is the last 311 | // three octets of the MAC address. Often in rural areas where the 312 | // only obvious source would be other automobiles. So suspect that 313 | // this is the default setup for a number of vehicle manufactures. 314 | val macSuffix = 315 | id.substring(id.length - 8).lowercase().replace(":", "") 316 | 317 | val blacklisted = 318 | lcSplit.any { blacklistWords.contains(it) } 319 | || blacklistStartsWith.any { lc.startsWith(it) } 320 | || blacklistEndsWith.any { lc.endsWith(it) } 321 | || blacklistEquals.contains(lc) 322 | // a few less simple checks 323 | || lcSplit.contains("moto") && note.startsWith("MOTO") // "MOTO9564" and "MOTO9916" seen 324 | || lcSplit.first() == "audi" // some cars seem to have this AP on-board 325 | || lc == macSuffix // Apparent default SSID name for many cars 326 | // deal with words not achievable with the blacklist sets, checking only if 327 | // lcSplit.contains() (for performance reasons) 328 | || (lcSplit.contains("admin") && lc.contains("admin@ms")) 329 | || (lcSplit.contains("guest") && lc.contains("guest@ms")) 330 | || (lcSplit.contains("contiki") && lc.contains("contiki-wifi")) // transport 331 | || (lcSplit.contains("interakti") && lc.contains("nsb_interakti")) // ??? 332 | || (lcSplit.contains("nvram") && lc.contains("nvram warning")) // transport 333 | 334 | if (DEBUG && blacklisted) Log.d(TAG, "blacklistWifi('$logString'): blacklisted") 335 | return blacklisted 336 | } 337 | 338 | /** 339 | * Our status can only make a small set of allowed transitions. Basically a simple 340 | * state machine. To assure our transitions are all legal, this routine is used for 341 | * all changes. 342 | * 343 | * @param newStatus The desired new status (state) 344 | * @param info Logging information for debug purposes 345 | */ 346 | private fun changeStatus(newStatus: EmitterStatus, info: String) { 347 | if (newStatus == status) return 348 | when (status) { 349 | EmitterStatus.STATUS_BLACKLISTED -> { } 350 | EmitterStatus.STATUS_CACHED, EmitterStatus.STATUS_CHANGED -> 351 | when (newStatus) { 352 | EmitterStatus.STATUS_BLACKLISTED, EmitterStatus.STATUS_CACHED, EmitterStatus.STATUS_CHANGED -> 353 | status = newStatus 354 | else -> { } 355 | } 356 | EmitterStatus.STATUS_NEW -> 357 | when (newStatus) { 358 | EmitterStatus.STATUS_BLACKLISTED, EmitterStatus.STATUS_CACHED -> 359 | status = newStatus 360 | else -> { } 361 | } 362 | EmitterStatus.STATUS_UNKNOWN -> 363 | when (newStatus) { 364 | EmitterStatus.STATUS_BLACKLISTED, EmitterStatus.STATUS_CACHED, EmitterStatus.STATUS_NEW -> 365 | status = newStatus 366 | else -> { } 367 | } 368 | } 369 | if (DEBUG) Log.d(TAG, "$info: tried switching to $newStatus, result: $status") 370 | return 371 | } 372 | } 373 | 374 | private val DEBUG = BuildConfig.DEBUG 375 | 376 | private const val TAG = "LocalNLP RfEmitter" 377 | 378 | private val splitRegex = "[^a-z]".toRegex() // for splitting SSID into "words" 379 | // use hashSets for fast blacklist*.contains() check 380 | private val blacklistWords = hashSetOf( 381 | "android", "ipad", "iphone", "phone", "motorola", "huawei", "nokia", "redmi", "realme", 382 | "honor", "oppo", "galaxy", "oneplus", // mobile tethering 383 | "mobile", // sounds like name for mobile hotspot 384 | "deinbus", "ecolines", "eurolines", "fernbus", "flixbus", "muenchenlinie", 385 | "postbus", "skanetrafiken", "oresundstag", "regiojet", "hotspotarriva", // transport 386 | 387 | // Per an instructional video on YouTube, recent (2014 and later) Chrysler-Fiat 388 | // vehicles have a SSID of the form "Chrysler uconnect xxxxxx" where xxxxxx 389 | // seems to be a hex digit string (suffix of BSSID?). 390 | "uconnect", // Chrysler built vehicles 391 | "chevy", // "Chevy Cruz 7774" and "Davids Chevy" seen. 392 | "silverado", // GMC Silverado. "Bryces Silverado" seen, maybe move to startsWith? 393 | "myvolvo", // Volvo in car WiFi, maybe move to startsWith? 394 | "bmw", // examples: BMW98303 CarPlay, My BMW Hotspot 8303, DIRECT-BMW 67727 395 | "skoda", // My Skoda 3358, Skoda_WLAN_5790 396 | "seat", // My SEAT 741, SEAT_WLAN 397 | "vw", // VW WLAN 9266, VW_WLAN, My VW 4025 398 | ) 399 | private val blacklistEquals = hashSetOf( 400 | "amtrak", "amtrakconnect", "cdwifi", "megabus", "westlan","wifi in de trein", 401 | "svciob", "oebb", "oebb-postbus", "dpmbfree", "telekom_ice", "db ic bus", 402 | "gkbgast", "mavstart-wifi", "wifionice", "wifi@db", "crosscountrywifi", 403 | "gwr wifi", "thalysnet", "_sncf_wifi_inoui", "_sncf_wifi_intercities", 404 | "normandietrainconnecte", "keolis nederland", "ouifi", "raillan", "vorwlan", 405 | "zssk wifi", "wifi zssk", "mavstart-wifi", "raaberbahn", "hotspot ic", "vmobil" // transport 406 | ) 407 | // and arrays if we just want to iterate 408 | private val blacklistStartsWith = arrayOf( 409 | "moto ", "lg aristo", "androidap", "vivo ", "mi ", // mobile tethering 410 | "cellspot", // T-Mobile US portable cell based WiFi 411 | "verizon", // Verizon mobile hotspot 412 | 413 | // Per some instructional videos on YouTube, recent (2015 and later) 414 | // General Motors built vehicles come with a default WiFi SSID of the 415 | // form "WiFi Hotspot 1234" where the 1234 is different for each car. 416 | "wifi hotspot ", // Default GM vehicle WiFi name 417 | 418 | // Per instructional video on YouTube, Mercedes cars have and SSID of 419 | // "MB WLAN nnnnn" where nnnnn is a 5 digit number, same for MB Hostspot and direct-mb hotspot 420 | "mb wlan ", "mb hotspot", "direct-mb hotspot", 421 | "westbahn ", "buswifi", "coachamerica", "disneylandresortexpress", 422 | "taxilinq", "transitwirelesswifi", // transport, maybe move some to words? 423 | "yicarcam", // Dashcam WiFi 424 | ) 425 | private val blacklistEndsWith = arrayOf( 426 | "corvette", // Chevy Corvette. "TS Corvette" seen. 427 | 428 | // General Motors built vehicles SSID can be changed but the recommended SSID to 429 | // change to is of the form "first_name vehicle_model" (e.g. "Bryces Silverado"). 430 | "truck", // "Morgans Truck" and "Wally Truck" seen 431 | "suburban", // Chevy/GMC Suburban. "Laura Suburban" seen 432 | "terrain", // GMC Terrain. "Nelson Terrain" seen 433 | "sierra", // GMC pickup. "dees sierra" seen 434 | "gmc wifi", // General Motors 435 | ) 436 | 437 | enum class EmitterStatus { 438 | STATUS_UNKNOWN, // Newly discovered emitter, no data for it at all 439 | STATUS_NEW, // Not in database but we've got location data for it 440 | STATUS_CHANGED, // In database but something has changed 441 | STATUS_CACHED, // In database no changes pending 442 | STATUS_BLACKLISTED // Has been blacklisted 443 | } 444 | 445 | // most recent location information about the emitter 446 | data class RfLocation( 447 | /** timestamp of most recent observation, like System.currentTimeMillis() */ 448 | val time: Long, 449 | /** elapsedRealtimeNanos of most recent observation */ 450 | val elapsedRealtimeNanos: Long, 451 | val lat: Double, 452 | val lon: Double, 453 | /** emitter radius, may be 0 */ 454 | val radius: Double, 455 | /** asu of most recent observation */ 456 | val asu: Int, 457 | val id: RfIdentification, 458 | /** whether we suspect the most recent observation might not be entirely correct */ 459 | val suspicious: Boolean, 460 | ) { 461 | /** emitter radius, but at least minimumRange for this EmitterType */ 462 | val accuracyEstimate: Double = radius.coerceAtLeast(id.rfType.getRfCharacteristics().minimumRange) 463 | } 464 | -------------------------------------------------------------------------------- /app/src/main/java/org/fitchfamily/android/dejavu/RfIdentification.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 | /** 24 | * Created by tfitch on 10/4/17. 25 | * modified by helium314 in 2022 26 | */ 27 | /** 28 | * This class forms a complete identification for a RF emitter. 29 | * 30 | * All it has are two fields: A rfID string that must be unique within a type 31 | * or class of emitters. And a rfType value that indicates the type of RF 32 | * emitter we are dealing with. 33 | */ 34 | class RfIdentification(val rfId: String, val rfType: EmitterType) { 35 | val uniqueId = when (rfType) { 36 | EmitterType.WLAN2, EmitterType.WLAN5, EmitterType.WLAN6 -> rfType.name + '/' + rfId 37 | else -> rfId 38 | } 39 | 40 | override fun toString(): String = uniqueId 41 | 42 | override fun equals(other: Any?): Boolean { 43 | if (this === other) return true 44 | if (other is RfIdentification) return uniqueId == other.uniqueId 45 | return false 46 | } 47 | 48 | override fun hashCode(): Int { 49 | return uniqueId.hashCode() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /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 = notNullIsland(loc.latitude, loc.longitude) 74 | // simplified check that should avoid distance calculation in almost every case where this return true 75 | fun notNullIsland(lat: Double, lon: Double): Boolean { 76 | return abs(lat) > NULL_ISLAND_DISTANCE_DEG 77 | || abs(lon) > NULL_ISLAND_DISTANCE_DEG 78 | || approximateDistance(lat, lon, 0.0, 0.0) > NULL_ISLAND_DISTANCE 79 | } 80 | 81 | // wifiManager.is6GHzBandSupported might be called to check whether it can be WLAN6 82 | // but wifiManager.is5GHzBandSupported incorrectly returns no on some devices, so can we trust 83 | // it to be correct for 6 GHz? 84 | // anyway, there might be a better way of determining WiFi type 85 | fun ScanResult.getWifiType(): EmitterType = 86 | when { 87 | frequency < 3000 -> EmitterType.WLAN2 // 2401 - 2495 MHz 88 | // 5945 can be WLAN5 and WLAN6, simply don't bother and assume WLAN5 for now 89 | frequency <= 5945 -> EmitterType.WLAN5 // 5030 - 5990 MHz, but at 5945 WLAN6 starts 90 | frequency > 6000 -> EmitterType.WLAN6 // 5945 - 7125 91 | frequency % 10 == 5 -> EmitterType.WLAN6 // in the overlapping range, WLAN6 frequencies end with 5 92 | else -> EmitterType.WLAN5 93 | } 94 | 95 | /** 96 | * 97 | * The collector service attempts to detect and not report moved/moving emitters. 98 | * But it (and thus our database) can't be perfect. This routine looks at all the 99 | * emitters and returns the largest subset (group) that are within a reasonable 100 | * distance of one another. 101 | * 102 | * The hope is that a single moved/moving emitters that is seen now but whose 103 | * location was detected miles away can be excluded from the set of APs 104 | * we use to determine where the phone is at this moment. 105 | * 106 | * We do this by creating collections of emitters where all the emitters in a group 107 | * are within a plausible distance of one another. A single emitters may end up 108 | * in multiple groups. When done, we return the largest group. 109 | * 110 | * If we are at the extreme limit of possible coverage (maximumRange) 111 | * from two emitters then those emitters could be a distance of 2*maximumRange apart. 112 | * So we will group the emitters based on that large distance. 113 | * 114 | * @param locations A collection of the coverages for the current observation set 115 | * @return The largest set of coverages found within the raw observations. That is 116 | * the most believable set of coverage areas. 117 | */ 118 | fun culledEmitters(locations: Collection): Set? { 119 | val groups = divideInGroups(locations) 120 | groups.maxByOrNull { it.size }?.let { result -> 121 | // if we only have one location, use it as long as it's not an invalid emitter 122 | if (locations.size == 1 && result.single().id.rfType != EmitterType.INVALID) { 123 | if (DEBUG) Log.d(TAG, "culledEmitters() - got only one location, use it") 124 | return result 125 | } 126 | // Determine minimum count for a valid group of emitters. 127 | // The RfEmitter class will have put the min count into the location 128 | // it provided. 129 | result.forEach { 130 | if (result.size >= it.id.rfType.getRfCharacteristics().minCount) 131 | return result 132 | } 133 | if (DEBUG) Log.d(TAG, "culledEmitters() - only got ${result.size}, but " + 134 | "${result.minOfOrNull { it.id.rfType.getRfCharacteristics().minCount }} are required") 135 | } 136 | return null 137 | } 138 | 139 | /** 140 | * Build a list of sets (or groups) each outer set member is a set of coverage of 141 | * reasonably near RF emitters. Basically we are grouping the raw observations 142 | * into clumps based on how believably close together they are. An outlying emitter 143 | * will likely be put into its own group. Our caller will take the largest set as 144 | * the most believable group of observations to use to compute a position. 145 | * 146 | * @param locations A set of RF emitter coverage records 147 | * @return A list of coverage sets. 148 | */ 149 | private fun divideInGroups(locations: Collection): List> { 150 | // Create bins 151 | val bins = locations.map { hashSetOf(it) } 152 | for (location in locations) { 153 | for (locationGroup in bins) { 154 | if (locationCompatibleWithGroup(location, locationGroup)) { 155 | locationGroup.add(location) 156 | } 157 | } 158 | } 159 | return bins 160 | } 161 | 162 | /** 163 | * Check to see if the coverage area (location) of an RF emitter is close 164 | * enough to others in a group that we can believably add it to the group. 165 | * @param location The coverage area of the candidate emitter 166 | * @param locGroup The coverage areas of the emitters already in the group 167 | * @return True if location is close to others in group 168 | */ 169 | private fun locationCompatibleWithGroup(location: RfLocation, locGroup: Set): Boolean { 170 | // If the location is within range of all current members of the 171 | // group, then we are compatible. 172 | for (other in locGroup) { 173 | // allow somewhat larger distance than sum of accuracies, looks like results are usually a bit better 174 | if (approximateDistance(location.lat, location.lon, other.lat, other.lon) > (location.accuracyEstimate + other.accuracyEstimate) * 1.25) { 175 | return false 176 | } 177 | } 178 | return true 179 | } 180 | 181 | /** 182 | * Shorter version of the original WeightedAverage, with adjusted weight to consider emitters 183 | * we don't know much about. 184 | * This ignores multiplying longitude accuracy by cosLat when converting to degrees, and 185 | * later dividing by cosLat when converting back to meters. It doesn't cancel out completely 186 | * because the used latitudes generally are slightly different, but differences are negligible 187 | * for our use. 188 | */ 189 | // main difference to the old WeightedAverage: accuracy is also influenced by how far 190 | // apart the emitters are (sounds more relevant than it is, due to only "compatible" locations 191 | // being used anyway) 192 | fun Collection.weightedAverage(): Location { 193 | val latitudes = DoubleArray(size) 194 | val longitudes = DoubleArray(size) 195 | val accuracies = DoubleArray(size) 196 | val weights = DoubleArray(size) 197 | forEachIndexed { i, it -> 198 | latitudes[i] = it.lat 199 | longitudes[i] = it.lon 200 | val minRange = it.id.rfType.getRfCharacteristics().minimumRange 201 | // significantly reduce asu if we don't really trust the location, but don't discard it 202 | val asu = if (it.suspicious) (it.asu / 4).coerceAtLeast(MINIMUM_ASU) else it.asu 203 | weights[i] = asu / it.accuracyEstimate 204 | 205 | // The actual accuracy we want to use for this location is an adjusted accuracyEstimate. 206 | // If asu is good, we're likely close to the emitter, so we can decrease accuracy value. 207 | // asuAdjustedAccuracy varies between minRange and accuracyEstimate 208 | // But at the same time, we may not have the full emitter in the database. 209 | // In this case, an accuracy improvement may actually result in an over-confident estimate, 210 | // which is not desirable. Thus we reduce the asuFactor if the emitter is much smaller than 211 | // it maximum range for its type. 212 | val rangeFactor = min(5 * it.radius / it.id.rfType.getRfCharacteristics().maximumRange, 1.0) 213 | val asuFactor = 1.0 - ((asu - MINIMUM_ASU) * 1.0 / MAXIMUM_ASU) * rangeFactor 214 | 215 | val asuAdjustedAccuracy = minRange + asuFactor * asuFactor * (it.accuracyEstimate - minRange) 216 | 217 | // 218 | // Our input has an accuracy based on the detection of the edge of the coverage area. 219 | // So assume that is a high (two sigma) probability and, worse, assume we can turn that 220 | // into normal distribution error statistic. We will assume our standard deviation (one 221 | // sigma) is half of our accuracy. 222 | //accuracies[i] = asuAdjustedAccuracy * METER_TO_DEG * 0.5 223 | // But we use the factor 0.7 instead, because 0.5 sometimes gives overly accurate results. 224 | // This makes accuracy worse if asu is low, and if range is close to minRange. The former 225 | // is desired, and the latter is a side effect that usually isn't that bad 226 | accuracies[i] = asuAdjustedAccuracy * METER_TO_DEG * 0.7 227 | } 228 | // set weighted means 229 | val latMean = weightedMean(latitudes, weights) 230 | val lonMean = weightedMean(longitudes, weights) 231 | // and variances, to use for accuracy 232 | val hasWifi = any { it.id.rfType in shortRangeEmitterTypes } 233 | val latVariance = weightedVariance(latMean, latitudes, accuracies, weights, hasWifi) 234 | val lonVariance = weightedVariance(lonMean, longitudes, accuracies, weights, hasWifi) 235 | val acc = (sqrt(latVariance + lonVariance) * DEG_TO_METER) 236 | // seen weirdly bad results if only 1 emitter is available, and we only have seen it in 237 | // very few locations -> need to catch this 238 | // similar if all WiFis are suspicious... don't trust it 239 | val allWifisSuspicious = hasWifi && none { !it.suspicious && it.id.rfType in shortRangeEmitterTypes } 240 | val reportAcc = acc * if (allWifisSuspicious || (size == 1 && first().radius < single().id.rfType.getRfCharacteristics().minimumRange)) 241 | 1.5 else 1.0 // factor 1.5 to approximately undo the factor 0.7 above 242 | return location(latMean, lonMean, reportAcc.toFloat()) 243 | } 244 | 245 | fun Collection.location(lat: Double, lon: Double, acc: Float): Location = 246 | Location(LOCATION_PROVIDER).apply { 247 | extras = Bundle().apply { putInt("AVERAGED_OF", size) } 248 | 249 | // set newest times 250 | time = maxOf { it.time } 251 | elapsedRealtimeNanos = maxOf { it.elapsedRealtimeNanos } 252 | 253 | latitude = lat 254 | longitude = lon 255 | accuracy = acc.coerceAtLeast(MINIMUM_BELIEVABLE_ACCURACY) 256 | } 257 | 258 | /** 259 | * @returns the weighted mean of the given positions, accuracies and weights 260 | */ 261 | private fun weightedMean(positions: DoubleArray, weights: DoubleArray): Double { 262 | var weightedSum = 0.0 263 | positions.forEachIndexed { i, position -> 264 | weightedSum += position * weights[i] 265 | } 266 | return weightedSum / weights.sum() 267 | } 268 | 269 | /** 270 | * @returns the weighted variance of the given positions, accuracies and weights. 271 | * Variance and not stdDev because we need to square it anyway 272 | * 273 | * Actually this is not really correct, but it's good enough... 274 | * What we want from accuracy: 275 | * more (very) similar locations should improve accuracy 276 | * positions far apart should give worse accuracy, even if the single accuracies are similar 277 | */ 278 | private fun weightedVariance(weightedMeanPosition: Double, positions: DoubleArray, accuracies: DoubleArray, weights: DoubleArray, betterAccuracy: Boolean): Double { 279 | // we have a situation like 280 | // https://stats.stackexchange.com/questions/454120/how-can-i-calculate-uncertainty-of-the-mean-of-a-set-of-samples-with-different-u#comment844099_454266 281 | // but we already have weights... so come up with something that gives reasonable results 282 | var weightedVarianceSum = 0.0 283 | positions.forEachIndexed { i, position -> 284 | weightedVarianceSum += if (betterAccuracy) { 285 | // usually 5-20% better accuracy, but often not nice if we don't have any wifis 286 | val dev = max(accuracies[i], abs(position - weightedMeanPosition)) 287 | weights[i] * weights[i] * dev * dev 288 | } else 289 | weights[i] * weights[i] * (accuracies[i] * accuracies[i] + (position - weightedMeanPosition) * (position - weightedMeanPosition)) 290 | } 291 | 292 | // this is not really variance, but still similar enough to claim it is 293 | // dividing by size should be fine... 294 | return weightedVarianceSum / weights.sumOf { it * it } 295 | } 296 | 297 | // weighted average with removing outliers (more than 2 accuracies away from median center) 298 | // and use only short range emitters if any are available 299 | fun Collection.medianCull(): Collection? { 300 | if (isEmpty()) return null 301 | // use trustworthy wifi results for median location, but only if at least 3 emitters 302 | // if we have less than 3 results, also use suspicious results 303 | // if we still have less than 3 results, use all 304 | // 3 results because with less there is a too high chance of bad median locations (see below) 305 | val emittersForMedian = filter { it.id.rfType in shortRangeEmitterTypes && !it.suspicious } 306 | .let { goodList -> 307 | if (goodList.size >= 3) goodList 308 | else this.filter { it.id.rfType in shortRangeEmitterTypes } 309 | .let { okList -> 310 | if (okList.size >= 3) okList 311 | else this 312 | } 313 | } 314 | // Take median of lat and lon separately because it simple. This can lead to unexpected and 315 | // bad results if emitters are very far apart. Ideally such cases should be caught in medianCullSafe. 316 | val latMedian = emittersForMedian.map { it.lat }.median() 317 | val lonMedian = emittersForMedian.map { it.lon }.median() 318 | // Use locations that are close enough to the median location (2 * their accuracy). 319 | // Maybe the factor 2 could be reduced to 1.5 or sth like this... but we really just want to 320 | // remove outliers, so it shouldn't matter too much. 321 | val closeToMedian = filter { approximateDistance(latMedian, lonMedian, it.lat, it.lon) < 2.0 * it.accuracyEstimate } 322 | if (DEBUG) Log.d(TAG, "medianCull() - using ${closeToMedian.size} of initially $size locations") 323 | return closeToMedian.ifEmpty { culledEmitters(this) } // fallback to original culledEmitters 324 | } 325 | 326 | private fun List.median() = sorted().let { 327 | if (size % 2 == 1) it[size / 2] 328 | else (it[size / 2] + it[(size - 1) / 2]) / 2 329 | } 330 | 331 | fun Collection.medianCullSafe(): Location? { 332 | val medianCull = medianCull() ?: return null // returns null if list is empty 333 | /* Need to decide whether to really use medianCull, because in some cases it produces 334 | * bad results. To detect such cases we use a more exhaustive check if : 335 | * a. Any locations have been removed, and the resulting locations does not fit with noCullLoc, 336 | * i.e. they are further apart than the smaller accuracy 337 | * b. Too many locations have been removed. This can happen if medianCullLoc is at some 338 | * bad location, e.g. between 2 WiFi groups, or it's messed up because lat and lon 339 | * are treated independently in medianCull() 340 | * c. All WiFi emitters have been removed. This should not happen, but still does in some cases 341 | * like when we have a single WiFi that is far away from mobile emitters 342 | * If any check returns true, we also create normalCullLoc and use whichever of the three 343 | * locations is closest to their center. 344 | */ 345 | if (medianCull.size == size) return this.weightedAverage() // nothing removed, all should be fine 346 | val medianCullLoc = medianCull.weightedAverage() 347 | val noCullLoc = weightedAverage() 348 | val d = approximateDistance(medianCullLoc.latitude, medianCullLoc.longitude, noCullLoc.latitude, noCullLoc.longitude) 349 | if (d > medianCullLoc.accuracy 350 | || d > noCullLoc.accuracy 351 | || medianCull.size <= size * 0.8 352 | || (medianCull.none { it.id.rfType in shortRangeEmitterTypes } && this.any { it.id.rfType in shortRangeEmitterTypes }) 353 | ) { 354 | // we have a potentially bad location -> check normal cull and no cull and compare 355 | val normalCullLoc = culledEmitters(this)?.weightedAverage() 356 | val locs = listOfNotNull(medianCullLoc, noCullLoc, normalCullLoc) 357 | val meanLat = locs.sumOf { it.latitude } / locs.size 358 | val meanLon = locs.sumOf { it.longitude } / locs.size 359 | val l = locs.minByOrNull { 360 | approximateDistance(meanLat, meanLon, it.latitude, it.longitude) 361 | } 362 | // this very often results in noCull, which may be much less accurate than the other 2 363 | // so try using medianCull location instead if it seems reasonably accurate 364 | if (l == noCullLoc && noCullLoc.accuracy > 2.0 * medianCullLoc.accuracy 365 | && approximateDistance(noCullLoc.latitude, noCullLoc.longitude, medianCullLoc.latitude, medianCullLoc.longitude) < noCullLoc.accuracy 366 | ) { 367 | if (DEBUG) Log.d(TAG, "medianCullSafe() - using medianCull because chosen noCull is close but much less accurate") 368 | return medianCullLoc 369 | } 370 | if (DEBUG) { 371 | if (l == medianCullLoc) 372 | Log.d(TAG, "medianCullSafe() - checked medianCull, still using") 373 | else 374 | Log.d(TAG, "medianCullSafe() - not using medianCull") 375 | } 376 | return l 377 | } 378 | return medianCullLoc 379 | } 380 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 16 | 19 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_notification.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 16 | 19 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/d834076955b936887ece07ba05dbf9d582363072/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/d834076955b936887ece07ba05dbf9d582363072/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/d834076955b936887ece07ba05dbf9d582363072/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/d834076955b936887ece07ba05dbf9d582363072/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/d834076955b936887ece07ba05dbf9d582363072/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/d834076955b936887ece07ba05dbf9d582363072/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/d834076955b936887ece07ba05dbf9d582363072/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/d834076955b936887ece07ba05dbf9d582363072/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/d834076955b936887ece07ba05dbf9d582363072/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/d834076955b936887ece07ba05dbf9d582363072/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Ein microG/UnifiedNlp Standortdienst der eine private Datenbank der Mobilfunkstationen und WLANS benutzt 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values-eo/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Pozici‑trova subservo microG/UnifiedNlp, kiu uzas privatan datumbazon de sendiloj Wi‑Fi kaj GSM 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values-fr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Un service de géolocalisation pour microG/Unified Network Location Provider utilisant une base de données privée stockée sur le téléphone de l\'utilisateur 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values-pl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Usługa lokalizacji microG/UnifiedNlp używająca prywatnej bazy danych nadajników na telefonie 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values-ru/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Геолокация по персональной локальной базе WiFi- и GSM-передатчиков 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values-uk/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Сервіс позиціювання для microG/UnifiedNlp, який використовую локальну базу даних джерел радіовипромінювання 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 一个使用设备自带的 RF 射频信号发射器实现的 microG/UnifiedNlp 位置提供器 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/appName.xml: -------------------------------------------------------------------------------- 1 | 2 | Local NLP Backend 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @string/pref_active_mode_off 5 | @string/pref_active_mode_low 6 | @string/pref_active_mode_medium 7 | @string/pref_active_mode_high 8 | @string/pref_active_mode_aggressive 9 | 10 | 11 | 12 | "0" 13 | "1" 14 | "2" 15 | "3" 16 | "4" 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #2576D2 4 | -------------------------------------------------------------------------------- /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 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/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/xml/preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 15 | 20 | 28 | 33 | 37 | 41 | 45 | 49 | 50 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | google() 5 | } 6 | dependencies { 7 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21" 8 | classpath 'com.android.tools.build:gradle:8.7.2' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | mavenCentral() 15 | google() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /fastlane/metadata/android/de/short_description.txt: -------------------------------------------------------------------------------- 1 | Ein Standortdienst für UnifiedNlp und microG der eine lokale Datenbank nutzt. 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/15.txt: -------------------------------------------------------------------------------- 1 | - Build specification to reduce size of released application. 2 | - Update build environment 3 | - Add Ukrainian translation 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/16.txt: -------------------------------------------------------------------------------- 1 | - Fix crash on empty set of seen emitters. 2 | - Fix some Lint identified items. -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/17.txt: -------------------------------------------------------------------------------- 1 | - Fix timing related crash on start up/shut down 2 | - Revisions to better support external GPS with faster report rates. 3 | - Revise database to allow same identifier on multiple emitter types. 4 | - Initial support for 5 GHz WLAN RF characteristics being different than 2.4 GHz WLAN. 5 | - Updated build tools and target API version -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/18.txt: -------------------------------------------------------------------------------- 1 | - Add Chinese translation (thanks to @Crystal-RainSlide) 2 | - Protect against external GPS giving locations near 0.0/0.0 3 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/19.txt: -------------------------------------------------------------------------------- 1 | - Update Gradle build environment 2 | - Revise checks for locations near lat/lon of 0,0 3 | - Ignore WLANs on trains and buses of transit agencies in southwest Sweden. Thanks to lbschenkel 4 | - Ignore Austrian train WLANs. Thanks to akallabeth -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/20.txt: -------------------------------------------------------------------------------- 1 | - Update gradle build environment 2 | - Revise list of WLAN/WiFi SSIDs to ignore 3 | - Add Esperanto and Polish translations -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/21.txt: -------------------------------------------------------------------------------- 1 | - Update gradle build environment 2 | - Add debug logging for detection of 5G WiFi/WLAN networds 3 | - Add some Czech, Austrian and Dutch transport WLANs to ignore list 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/23.txt: -------------------------------------------------------------------------------- 1 | - New app and package names. 2 | - New icon (modified from: https://thenounproject.com/icon/38241). 3 | - Some small bug fixes. 4 | - Update and actually use the WiFi blacklist. 5 | - Faster, but less exact distance calculations. For the used distances up to 100 km, the differences are negligible. 6 | - Ignore cell emitters with invalid LAC. 7 | - 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. 8 | - Consider that LTE and 3G cells are usually smaller than GSM cells. 9 | - 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. 10 | - Adjusted how position and accuracy are determined. 11 | - 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. 12 | - Blacklist emitters with suspiciously high radius, as they may actually be mobile hotspots. 13 | - Don't use outdated WiFi scan results if scan is not successful. This helps especially against WiFi throttling introduced in Android 9. 14 | - Consider signal strength when estimating accuracy. 15 | - 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. 16 | - 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. 17 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/24.txt: -------------------------------------------------------------------------------- 1 | - Progress bars for import and export 2 | - fix MLS import for LTE cells 3 | - fix import of files exported with Local NLP Backend 4 | - faster import 5 | - reworked database code 6 | - upgrade dependencies 7 | - prepare for API upgrade (will remove deprecated getNeighboringCellInfo function, which may be used by some old devices) 8 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/25.txt: -------------------------------------------------------------------------------- 1 | - fix crash when showing nearby emitters 2 | - slightly less ugly buttons when showing nearby emitters 3 | 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/26.txt: -------------------------------------------------------------------------------- 1 | - Fix database import from content URI. Now import should work on all devices. 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/27.txt: -------------------------------------------------------------------------------- 1 | - Update blacklist. 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/28.txt: -------------------------------------------------------------------------------- 1 | - Manually blacklist emitter when showing nearby emitters. 2 | - Active mode: enable GPS when emitters are found, but none has a known location (disabled by default). 3 | - Update blacklist. 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/29.txt: -------------------------------------------------------------------------------- 1 | - Fix crashes 2 | - Upgrade to API 33 3 | - Add support for 5G and TDSCDMA cells 4 | - This is a beta version due to lack of devices using API 29+ or 5G / TDSCDMA, which means the new features haven't been tested 5 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/30.txt: -------------------------------------------------------------------------------- 1 | - different application id for debug builds 2 | - fix mobile emitters not being stored on some devices 3 | - improve storing/updating emitters, especially when using active mode 4 | - extend blacklist 5 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/31.txt: -------------------------------------------------------------------------------- 1 | - Extend blacklist 2 | - Allow more aggressive active mode settings: fill the database better, but may increase battery use 3 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/32.txt: -------------------------------------------------------------------------------- 1 | - Fix not (properly) asking for background location, resulting in no location permissions being asked on Android 11+ 2 | - Update microG NLP API and other dependencies 3 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/33.txt: -------------------------------------------------------------------------------- 1 | - Fix MLS import not working without MCC filter 2 | - Support placeholder for simplified MCC filtering 3 | - Fix bugs when importing files 4 | - Clarify that OpenCelliD files can be used too, as the format is same as MLS 5 | - Switch from Light theme to DayNight theme 6 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/34.txt: -------------------------------------------------------------------------------- 1 | - Notification text for active mode now contains name of emitter that triggered the scan 2 | - Keep screen on during import / export operations 3 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/35.txt: -------------------------------------------------------------------------------- 1 | - Crash fix 2 | - Small UI changes when viewing nearby emitters and emitter details 3 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/36.txt: -------------------------------------------------------------------------------- 1 | - Handle geo uris: allows adding emitters as if a GPS location was received at the indicated location 2 | - Improved blacklisting of unbelievably large emitters 3 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/38.txt: -------------------------------------------------------------------------------- 1 | - Extended blacklist (thanks to Sorunome) 2 | - Avoid searching nearby WiFis if GPS accuracy isn't good enough 3 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/39.txt: -------------------------------------------------------------------------------- 1 | - Import MLS / OpenCelliD lists without header 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/40.txt: -------------------------------------------------------------------------------- 1 | - Extend blacklist 2 | - Avoid crashes due to invalid emitter type 3 | - Upgrade dependencies 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | The backend passively monitors the GPS and scans for nearby WiFis and mobile cells/towers. From this a database of emitter locations is created. 2 | When UnifiedNlp / microG request a location from Local NLP Backend, a scan for nearby emitter is initiated and a location determined based on the scan results. 3 | Local NLP Backend is a fork of the Déjà Vu NLP Backend with some improvements and a crude UI for configuration and importing / exporting data, including cell lists from MLS or OpenCelliD. 4 | 5 | This backend uses no network data. All data acquired by the phone stays on the phone, though it may be exported manually. 6 | 7 | How to use: 8 | 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. 9 | If you have a Déjà Vu database (you'll need root privileged 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. 10 | Note that the local database needs to be filled, either using GPS or by importing data, before Local NLP Backend can provide locations! 11 | 12 | In order to speed up building the database, LocalNLP has an optional active mode that enabled GPS when there is no known emitter nearby (low setting) or when any unknown emitter is found (aggressive setting). 13 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/d834076955b936887ece07ba05dbf9d582363072/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/d834076955b936887ece07ba05dbf9d582363072/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Location provider for UnifiedNlp and microG using only local data. 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | Local NLP Backend 2 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | android.enableJetifier=false 13 | android.useAndroidX=true 14 | org.gradle.jvmargs=-Xmx1536m 15 | 16 | # When configured, Gradle will run in incubating parallel mode. 17 | # This option should only be used with decoupled projects. More details, visit 18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 19 | # org.gradle.parallel=true 20 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/d834076955b936887ece07ba05dbf9d582363072/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /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 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /res/Geolocation_-_The_Noun_Project.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/Geolocation_-_The_Noun_Project_mod.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Layer 1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /res/authors.txt: -------------------------------------------------------------------------------- 1 | Geolocation_-_The_Noun_Project.svg 2 | author: five by five, https://thenounproject.com/icon//FivebyFive 3 | license: CC0 1.0 4 | url: https://commons.wikimedia.org/wiki/File:Geolocation_-_The_Noun_Project.svg / https://thenounproject.com/icon/38241 5 | used as base for launcher icon 6 | 7 | Geolocation_-_The_Noun_Project_mod.svg 8 | author: helium314 / five by five 9 | license: CC0 1.0 10 | modified from Geolocation_-_The_Noun_Project.svg 11 | used as launcher icon 12 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------