├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── appName.xml │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── styles.xml │ │ │ │ ├── arrays.xml │ │ │ │ └── strings.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-zh-rCN │ │ │ │ └── strings.xml │ │ │ ├── values-ru │ │ │ │ └── strings.xml │ │ │ ├── values-eo │ │ │ │ └── strings.xml │ │ │ ├── values-pl │ │ │ │ └── strings.xml │ │ │ ├── values-de │ │ │ │ └── strings.xml │ │ │ ├── values-uk │ │ │ │ └── strings.xml │ │ │ ├── values-fr │ │ │ │ └── strings.xml │ │ │ ├── xml │ │ │ │ └── preferences.xml │ │ │ └── drawable │ │ │ │ ├── ic_notification.xml │ │ │ │ ├── ic_launcher_foreground.xml │ │ │ │ └── ic_launcher_background.xml │ │ ├── java │ │ │ └── org │ │ │ │ └── fitchfamily │ │ │ │ └── android │ │ │ │ └── dejavu │ │ │ │ ├── RfIdentification.kt │ │ │ │ ├── HandleGeoUriActivity.kt │ │ │ │ ├── Observation.kt │ │ │ │ ├── BoundingBox.kt │ │ │ │ ├── RfCharacteristics.kt │ │ │ │ ├── Kalman1Dim.java │ │ │ │ ├── Cache.kt │ │ │ │ ├── Kalman.java │ │ │ │ ├── GpsMonitor.kt │ │ │ │ ├── Util.kt │ │ │ │ ├── Database.kt │ │ │ │ └── RfEmitter.kt │ │ └── AndroidManifest.xml │ └── debug │ │ └── res │ │ └── values │ │ └── appName.xml ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── fastlane └── metadata │ └── android │ ├── en-US │ ├── title.txt │ ├── changelogs │ │ ├── 27.txt │ │ ├── 39.txt │ │ ├── 16.txt │ │ ├── 26.txt │ │ ├── 35.txt │ │ ├── 40.txt │ │ ├── 25.txt │ │ ├── 38.txt │ │ ├── 20.txt │ │ ├── 15.txt │ │ ├── 18.txt │ │ ├── 31.txt │ │ ├── 34.txt │ │ ├── 36.txt │ │ ├── 21.txt │ │ ├── 32.txt │ │ ├── 28.txt │ │ ├── 30.txt │ │ ├── 29.txt │ │ ├── 19.txt │ │ ├── 33.txt │ │ ├── 17.txt │ │ ├── 24.txt │ │ └── 23.txt │ ├── short_description.txt │ ├── images │ │ ├── icon.png │ │ └── phoneScreenshots │ │ │ └── 1.png │ └── full_description.txt │ └── de │ └── short_description.txt ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── res ├── authors.txt ├── Geolocation_-_The_Noun_Project_mod.svg └── Geolocation_-_The_Noun_Project.svg ├── gradle.properties ├── CODE_OF_CONDUCT.md ├── gradlew.bat ├── CHANGELOG.md ├── gradlew └── README.md /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | Local NLP Backend 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/27.txt: -------------------------------------------------------------------------------- 1 | - Update blacklist. 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/39.txt: -------------------------------------------------------------------------------- 1 | - Import MLS / OpenCelliD lists without header 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Location provider for UnifiedNlp and microG using only local data. 2 | -------------------------------------------------------------------------------- /app/src/main/res/values/appName.xml: -------------------------------------------------------------------------------- 1 | 2 | Local NLP Backend 3 | 4 | 5 | -------------------------------------------------------------------------------- /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/16.txt: -------------------------------------------------------------------------------- 1 | - Fix crash on empty set of seen emitters. 2 | - Fix some Lint identified items. -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/26.txt: -------------------------------------------------------------------------------- 1 | - Fix database import from content URI. Now import should work on all devices. 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/debug/res/values/appName.xml: -------------------------------------------------------------------------------- 1 | 2 | Local NLP Backend Debug 3 | 4 | -------------------------------------------------------------------------------- /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/40.txt: -------------------------------------------------------------------------------- 1 | - Extend blacklist 2 | - Avoid crashes due to invalid emitter type 3 | - Upgrade dependencies 4 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/HEAD/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 一个使用设备自带的 RF 射频信号发射器实现的 microG/UnifiedNlp 位置提供器 3 | 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /app/src/main/res/values-ru/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Геолокация по персональной локальной базе WiFi- и GSM-передатчиков 3 | 4 | -------------------------------------------------------------------------------- /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/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/31.txt: -------------------------------------------------------------------------------- 1 | - Extend blacklist 2 | - Allow more aggressive active mode settings: fill the database better, but may increase battery use 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #2576D2 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helium314/Local-NLP-Backend/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-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-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Ein microG/UnifiedNlp Standortdienst der eine private Datenbank der Mobilfunkstationen und WLANS benutzt 3 | 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/res/values-uk/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Сервіс позиціювання для microG/UnifiedNlp, який використовую локальну базу даних джерел радіовипромінювання 3 | 4 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=61ad310d3c7d3e5da131b76bbf22b5a4c0786e9d892dae8c1658d4b484de3caa 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 43 11 | versionName "1.2.15" 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.2' 51 | } 52 | -------------------------------------------------------------------------------- /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 | 4 | 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. 5 | 6 | This backend uses no network data. All data acquired by the phone stays on the phone, though it may be exported manually. 7 | 8 | How to use: 9 | 10 | 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. 11 | 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. 12 | Note that the local database needs to be filled, either using GPS or by importing data, before Local NLP Backend can provide locations! 13 | 14 | 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). 15 | 16 | Note that microG has stopped supporting UnifiedNlp backends with 0.2.28. 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. 17 | Personally I use 0.2.10, as with later versions location backends stop providing locations after some time. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /res/Geolocation_-_The_Noun_Project_mod.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Layer 1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /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/res/xml/preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 15 | 20 | 28 | 33 | 37 | 41 | 45 | 49 | 50 | -------------------------------------------------------------------------------- /res/Geolocation_-_The_Noun_Project.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/res/drawable/ic_notification.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 16 | 19 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 16 | 19 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /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/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/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/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | A microG/UnifiedNlp location provider backend using private on phone RF emitter database 3 | 4 | 5 | Export data 6 | Write all data to a CSV file 7 | Export is only supported on KitKat and above 8 | Export canceled 9 | Exporting… 10 | Export finished 11 | Error while exporting: %s 12 | Use Kalman filter for GPS location 13 | Recommended for devices with bad GPS 14 | Use cell tower locations 15 | When disabled, scans for cell towers will not happen 16 | Use WiFi locations 17 | When disabled, WiFi scans will not happen 18 | Active mode 19 | Enable GPS when unknown emitters are found (to fill up the database) 20 | Off 21 | Low - Enable GPS only when emitters are found, but no location could be determined at all 22 | Medium - Also enable GPS when WiFi emitters were found, but none of them could be used to determine a location 23 | High - Like above, but require better GPS accuracy (to store 5 GHz WiFis) 24 | Aggressive - Enable GPS when unknown emitters are found. Very likely to cause excessive battery drain 25 | Active mode GPS timeout 26 | Active mode: GPS on. Scanning because of %s and %d others 27 | Show nearby emitters 28 | Scans for emitters and displays result with additional information 29 | Discard bad emitters 30 | Decide which emitters should be discarded in case of inconsistent locations 31 | Default (used in Déjà Vu): only use the largest consistent group of emitters. This usually gives good accuracy, but occasionally produces wrong locations.\n\n 32 | Median: discard emitters that are unbelievably far from the median location: somewhat worse accuracy than default, but reduced chance of wrong locations.\n\n 33 | Use all emitters: sensitive to extreme outliers and often the least accurate of the three, but most likely to contain the actual location inside the accuracy circle.\n\n 34 | Often all 3 methods give (nearly) the same results, expect to see a difference only in some cases.\n\n 35 | Current setting: %s 36 | 37 | Default 38 | Median 39 | Use all 40 | Import data 41 | Select CSV created from export, Déjà Vu / Local NLP Backend database or MLS / OpenCelliD csv list 42 | Error: file format unknown 43 | Error parsing line %s 44 | Error importing database: %s 45 | How to handle emitters that already exist in local database? 46 | Replace local emitters 47 | Keep local emitters unchanged 48 | Merge emitters 49 | Updating database… 50 | Use %s 51 | Import from csv file 52 | Enter country codes (MCC) to import, comma separated. Use "x" as placeholder for digits 0–9. Leave blank to import all. 53 | Importing… 54 | Import canceled, no changes made 55 | %1$d imported, %2$d skipped 56 | Import finished 57 | Scanning… 58 | Scanning failed, maybe the backend service is disabled. Try disabling and enabling again. 59 | Nearby emitters 60 | Details for emitter %s 61 | Emitter type: %s 62 | WiFi SSID: %s 63 | Center: latitude %1$.5f, longitude %2$.5f 64 | Width east-west: %.2f m 65 | Width north-south: %.2f m 66 | Signal: %d / 5 67 | This emitter is blacklisted 68 | This emitter is not in the database 69 | Blacklist this emitter 70 | Delete emitter %s? 71 | Delete 72 | Network location provider not available 73 | 74 | 75 | Please enable Local NLP Backend again so it may ask for background location permission 76 | 77 | Scan and insert emitters into database at this location?\n 78 | latitude: %1$.5f\n 79 | longitude: %2$.5f 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /app/src/main/java/org/fitchfamily/android/dejavu/Kalman1Dim.java: -------------------------------------------------------------------------------- 1 | package org.fitchfamily.android.dejavu; 2 | /* 3 | * DejaVu - A location provider backend for microG/UnifiedNlp 4 | */ 5 | 6 | /** 7 | * Created by tfitch on 8/31/17. 8 | */ 9 | 10 | /* 11 | * This package inspired and largely copied from 12 | * https://github.com/villoren/KalmanLocationManager.git 13 | */ 14 | 15 | /** 16 | * Copyright (c) 2014 Renato Villone 17 | * 18 | * Permission is hereby granted, free of charge, to any person obtaining a copy 19 | * of this software and associated documentation files (the "Software"), to deal 20 | * in the Software without restriction, including without limitation the rights 21 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 22 | * copies of the Software, and to permit persons to whom the Software is 23 | * furnished to do so, subject to the following conditions: 24 | * 25 | * The above copyright notice and this permission notice shall be included in all 26 | * copies or substantial portions of the Software. 27 | * 28 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 34 | * SOFTWARE. 35 | * 36 | * Changes and modifications to this code: 37 | * Copyright (C) 2017 Tod Fitch 38 | * 39 | * This program is Free Software: you can redistribute it and/or modify 40 | * it under the terms of the GNU General Public License as 41 | * published by the Free Software Foundation, either version 3 of the 42 | * License, or (at your option) any later version. 43 | * 44 | * This program is distributed in the hope that it will be useful, 45 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 46 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 47 | * GNU General Public License for more details. 48 | * 49 | * You should have received a copy of the GNU General Public License 50 | * along with this program. If not, see . 51 | */ 52 | 53 | 54 | class Kalman1Dim { 55 | private final static double TIME_SECOND = 1000.0; // One second in milliseconds 56 | 57 | /** 58 | * Minimal time step. 59 | * 60 | * Assume 200 KPH (55.6 m/s) and a maximum accuracy of 3 meters, then there is no need 61 | * to update the filter any faster than 166.7 ms. 62 | * 63 | */ 64 | private final static long TIME_STEP_MS = 150; 65 | 66 | /** 67 | * Last prediction time 68 | */ 69 | private long mPredTime; 70 | 71 | /** 72 | * Time step. Computed from differences in prediction times. 73 | */ 74 | private final double mt, mt2, mt2d2, mt3d2, mt4d4; 75 | 76 | /** 77 | * Process noise covariance. Computed from time step and process noise 78 | */ 79 | private final double mQa, mQb, mQc, mQd; 80 | 81 | /** 82 | * Estimated state 83 | */ 84 | private double mXa, mXb; 85 | 86 | /** 87 | * Estimated covariance 88 | */ 89 | private double mPa, mPb, mPc, mPd; 90 | 91 | 92 | /** 93 | * Create a single dimension kalman filter. 94 | * 95 | * @param processNoise Standard deviation to calculate noise covariance from. 96 | * @param timeMillisec The time the filter is started. 97 | */ 98 | public Kalman1Dim(double processNoise, long timeMillisec) { 99 | double mProcessNoise = processNoise; 100 | 101 | mPredTime = timeMillisec; 102 | 103 | mt = ((double)TIME_STEP_MS) / TIME_SECOND; 104 | mt2 = mt * mt; 105 | mt2d2 = mt2 / 2.0; 106 | mt3d2 = mt2 * mt / 2.0; 107 | mt4d4 = mt2 * mt2 / 4.0; 108 | 109 | // Process noise covariance 110 | double n2 = mProcessNoise * mProcessNoise; 111 | mQa = n2 * mt4d4; 112 | mQb = n2 * mt3d2; 113 | mQc = mQb; 114 | mQd = n2 * mt2; 115 | 116 | // Estimated covariance 117 | mPa = mQa; 118 | mPb = mQb; 119 | mPc = mQc; 120 | mPd = mQd; 121 | } 122 | 123 | /** 124 | * Reset the filter to the given state. 125 | *

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