From 515a9b23c43d95f10fb236d2541e8efb189f5f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Hur=C3=BDn?= Date: Sun, 16 Apr 2023 10:14:47 +0200 Subject: [PATCH] Added dejavu and nominatim backends, added logging --- api/build.gradle | 4 +- .../nlp/api/AbstractBackendService.java | 16 +- .../nlp/api/GeocoderBackendService.java | 4 +- .../nlp/api/HelperLocationBackendService.java | 6 +- .../nlp/api/LocationBackendService.java | 12 +- app-dejavu/.gitignore | 1 + app-dejavu/build.gradle | 38 + app-dejavu/proguard-rules.pro | 25 + app-dejavu/src/main/AndroidManifest.xml | 35 + .../nlp/backend/dejavu/BackendService.java | 1265 +++++++++++++++++ .../org/microg/nlp/backend/dejavu/Cache.java | 188 +++ .../org/microg/nlp/backend/dejavu/Kalman.java | 217 +++ .../microg/nlp/backend/dejavu/Kalman1Dim.java | 236 +++ .../microg/nlp/backend/dejavu/LogToFile.java | 130 ++ .../nlp/backend/dejavu/WeightedAverage.java | 142 ++ .../backend/dejavu/database/BoundingBox.java | 187 +++ .../nlp/backend/dejavu/database/Database.java | 444 ++++++ .../backend/dejavu/database/Observation.java | 118 ++ .../backend/dejavu/database/RfEmitter.java | 787 ++++++++++ .../dejavu/database/RfIdentification.java | 120 ++ .../nlp/backend/dejavu/ui/MainActivity.java | 109 ++ .../src/main/res/layout/activity_main.xml | 47 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 4178 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 8334 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2818 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2498 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 5587 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 10747 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 8473 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 19998 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 11721 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 26132 bytes app-dejavu/src/main/res/values/colors.xml | 6 + app-dejavu/src/main/res/values/strings.xml | 4 + .../android/dejavu/ExampleUnitTest.java | 17 + app-nominatim/build.gradle | 78 + app-nominatim/src/main/AndroidManifest.xml | 29 + .../nlp/backend/nominatim/BackendService.java | 328 +++++ .../backend/nominatim/SettingsActivity.java | 94 ++ .../main/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 4995 bytes .../main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 3149 bytes .../main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 7070 bytes .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 10934 bytes .../main/res/drawable-xxxhdpi/ic_launcher.png | Bin 0 -> 15877 bytes app-nominatim/src/main/res/values/arrays.xml | 7 + app-nominatim/src/main/res/values/strings.xml | 10 + .../src/main/res/xml/preferences.xml | 25 + app/build.gradle | 81 ++ .../UnifiedNlpStandalone/AndroidManifest.xml | 46 + .../UnifiedNlpStandalone/res/values/bools.xml | 21 + app/src/main/AndroidManifest.xml | 53 + app/src/main/res/mipmap-hdpi/ic_nlp_app.png | Bin 0 -> 2585 bytes .../main/res/mipmap-hdpi/ic_nlp_settings.png | Bin 0 -> 4324 bytes app/src/main/res/mipmap-mdpi/ic_nlp_app.png | Bin 0 -> 1723 bytes .../main/res/mipmap-mdpi/ic_nlp_settings.png | Bin 0 -> 2462 bytes app/src/main/res/mipmap-xhdpi/ic_nlp_app.png | Bin 0 -> 3496 bytes .../main/res/mipmap-xhdpi/ic_nlp_settings.png | Bin 0 -> 5991 bytes app/src/main/res/mipmap-xxhdpi/ic_nlp_app.png | Bin 0 -> 5531 bytes .../res/mipmap-xxhdpi/ic_nlp_settings.png | Bin 0 -> 10168 bytes .../main/res/mipmap-xxxhdpi/ic_nlp_app.png | Bin 0 -> 8121 bytes .../res/mipmap-xxxhdpi/ic_nlp_settings.png | Bin 0 -> 14699 bytes app/src/main/res/values/bools.xml | 21 + app/src/main/res/values/log_file_lasting.xml | 19 + app/src/main/res/values/strings.xml | 81 ++ build.gradle | 4 +- client/build.gradle | 4 +- geocode-v1/build.gradle | 4 +- gradle.properties | 2 +- location-v2/build.gradle | 4 +- location-v3/build.gradle | 4 +- service-api/build.gradle | 4 +- .../org/microg/nlp/service/api/Constants.java | 3 + service/build.gradle | 7 +- .../nlp/service/AbstractBackendHelper.kt | 26 +- .../nlp/service/AsyncGeocoderBackend.kt | 56 +- .../nlp/service/AsyncLocationBackend.kt | 55 +- .../org/microg/nlp/service/GeocodeFuser.kt | 41 +- .../org/microg/nlp/service/LocationFuser.kt | 107 +- .../org/microg/nlp/service/LocationService.kt | 25 +- .../org/microg/nlp/service/LogToFile.java | 131 ++ .../nlp/service/UnifiedLocationServiceRoot.kt | 2 + settings.gradle | 4 +- ui/build.gradle | 9 +- .../org/microg/nlp/ui/BackendConfiguration.kt | 77 +- .../microg/nlp/ui/BackendDetailsFragment.kt | 47 +- .../org/microg/nlp/ui/BackendListFragment.kt | 109 +- .../microg/nlp/ui/BackendSettingsActivity.kt | 2 + .../org/microg/nlp/ui/model/BackendInfo.kt | 14 +- 88 files changed, 5529 insertions(+), 263 deletions(-) create mode 100644 app-dejavu/.gitignore create mode 100644 app-dejavu/build.gradle create mode 100644 app-dejavu/proguard-rules.pro create mode 100644 app-dejavu/src/main/AndroidManifest.xml create mode 100644 app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/BackendService.java create mode 100644 app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/Cache.java create mode 100644 app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/Kalman.java create mode 100644 app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/Kalman1Dim.java create mode 100644 app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/LogToFile.java create mode 100644 app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/WeightedAverage.java create mode 100644 app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/database/BoundingBox.java create mode 100644 app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/database/Database.java create mode 100644 app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/database/Observation.java create mode 100644 app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/database/RfEmitter.java create mode 100644 app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/database/RfIdentification.java create mode 100644 app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/ui/MainActivity.java create mode 100644 app-dejavu/src/main/res/layout/activity_main.xml create mode 100644 app-dejavu/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100755 app-dejavu/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app-dejavu/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app-dejavu/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app-dejavu/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100755 app-dejavu/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app-dejavu/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100755 app-dejavu/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app-dejavu/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100755 app-dejavu/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app-dejavu/src/main/res/values/colors.xml create mode 100644 app-dejavu/src/main/res/values/strings.xml create mode 100644 app-dejavu/src/test/java/org/fitchfamily/android/dejavu/ExampleUnitTest.java create mode 100644 app-nominatim/build.gradle create mode 100644 app-nominatim/src/main/AndroidManifest.xml create mode 100644 app-nominatim/src/main/java/org/microg/nlp/backend/nominatim/BackendService.java create mode 100644 app-nominatim/src/main/java/org/microg/nlp/backend/nominatim/SettingsActivity.java create mode 100644 app-nominatim/src/main/res/drawable-hdpi/ic_launcher.png create mode 100644 app-nominatim/src/main/res/drawable-mdpi/ic_launcher.png create mode 100644 app-nominatim/src/main/res/drawable-xhdpi/ic_launcher.png create mode 100644 app-nominatim/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100644 app-nominatim/src/main/res/drawable-xxxhdpi/ic_launcher.png create mode 100644 app-nominatim/src/main/res/values/arrays.xml create mode 100644 app-nominatim/src/main/res/values/strings.xml create mode 100644 app-nominatim/src/main/res/xml/preferences.xml create mode 100644 app/build.gradle create mode 100644 app/src/UnifiedNlpStandalone/AndroidManifest.xml create mode 100644 app/src/UnifiedNlpStandalone/res/values/bools.xml create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_nlp_app.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_nlp_settings.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_nlp_app.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_nlp_settings.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_nlp_app.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_nlp_settings.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_nlp_app.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_nlp_settings.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_nlp_app.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_nlp_settings.png create mode 100644 app/src/main/res/values/bools.xml create mode 100644 app/src/main/res/values/log_file_lasting.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 service/src/main/kotlin/org/microg/nlp/service/LogToFile.java diff --git a/api/build.gradle b/api/build.gradle index c8d9ef2..bfdc6f2 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -18,8 +18,8 @@ android { } compileOptions { - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + sourceCompatibility = 11 + targetCompatibility = 11 } } diff --git a/api/src/main/java/org/microg/nlp/api/AbstractBackendService.java b/api/src/main/java/org/microg/nlp/api/AbstractBackendService.java index d893c40..289d4ae 100644 --- a/api/src/main/java/org/microg/nlp/api/AbstractBackendService.java +++ b/api/src/main/java/org/microg/nlp/api/AbstractBackendService.java @@ -23,7 +23,7 @@ public IBinder onBind(Intent intent) { /** * Called after a connection was setup */ - protected void onOpen() { + public void onOpen() { } @@ -34,17 +34,25 @@ protected void onClose() { } - protected Intent getInitIntent() { + public Intent getInitIntent() { return null; } @SuppressWarnings("SameReturnValue") - protected Intent getSettingsIntent() { + public Intent getSettingsIntent() { return null; } @SuppressWarnings("SameReturnValue") - protected Intent getAboutIntent() { + public Intent getAboutIntent() { + return null; + } + + public String getBackendName() { + return null; + } + + public String getDescription() { return null; } diff --git a/api/src/main/java/org/microg/nlp/api/GeocoderBackendService.java b/api/src/main/java/org/microg/nlp/api/GeocoderBackendService.java index 1cea334..606f7fa 100644 --- a/api/src/main/java/org/microg/nlp/api/GeocoderBackendService.java +++ b/api/src/main/java/org/microg/nlp/api/GeocoderBackendService.java @@ -36,7 +36,7 @@ public void disconnect() { * address should be localized in * @see android.location.Geocoder#getFromLocation(double, double, int) */ - protected abstract List
getFromLocation(double latitude, double longitude, int maxResults, String locale); + public abstract List
getFromLocation(double latitude, double longitude, int maxResults, String locale); protected List
getFromLocation(double latitude, double longitude, int maxResults, String locale, Bundle options) { return getFromLocation(latitude, longitude, maxResults, locale); @@ -47,7 +47,7 @@ protected List
getFromLocation(double latitude, double longitude, int m * address should be localized in * @see android.location.Geocoder#getFromLocationName(String, int, double, double, double, double) */ - protected abstract List
getFromLocationName(String locationName, int maxResults, double lowerLeftLatitude, double lowerLeftLongitude, double upperRightLatitude, double upperRightLongitude, String locale); + public abstract List
getFromLocationName(String locationName, int maxResults, double lowerLeftLatitude, double lowerLeftLongitude, double upperRightLatitude, double upperRightLongitude, String locale); protected List
getFromLocationName(String locationName, int maxResults, double lowerLeftLatitude, double lowerLeftLongitude, double upperRightLatitude, double upperRightLongitude, String locale, Bundle options) { diff --git a/api/src/main/java/org/microg/nlp/api/HelperLocationBackendService.java b/api/src/main/java/org/microg/nlp/api/HelperLocationBackendService.java index 83e68b9..c0318ca 100644 --- a/api/src/main/java/org/microg/nlp/api/HelperLocationBackendService.java +++ b/api/src/main/java/org/microg/nlp/api/HelperLocationBackendService.java @@ -44,7 +44,7 @@ public synchronized void removeHelpers() { } @Override - protected synchronized void onOpen() { + public synchronized void onOpen() { for (AbstractBackendHelper helper : helpers) { helper.onOpen(); } @@ -60,7 +60,7 @@ protected synchronized void onClose() { } @Override - protected synchronized Location update() { + public synchronized Location update() { for (AbstractBackendHelper helper : helpers) { helper.onUpdate(); } @@ -68,7 +68,7 @@ protected synchronized Location update() { } @Override - protected Intent getInitIntent() { + public Intent getInitIntent() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // Consider permissions List perms = new LinkedList<>(); diff --git a/api/src/main/java/org/microg/nlp/api/LocationBackendService.java b/api/src/main/java/org/microg/nlp/api/LocationBackendService.java index 83c8274..83a0c6b 100644 --- a/api/src/main/java/org/microg/nlp/api/LocationBackendService.java +++ b/api/src/main/java/org/microg/nlp/api/LocationBackendService.java @@ -14,7 +14,7 @@ @SuppressWarnings({"WeakerAccess", "unused"}) public abstract class LocationBackendService extends AbstractBackendService { - private LocationCallback callback; + public LocationCallback callback; private Location waiting; /** @@ -28,7 +28,7 @@ public abstract class LocationBackendService extends AbstractBackendService { * @return a new {@link android.location.Location} instance or null if not available. */ @SuppressWarnings("SameReturnValue") - protected Location update() { + public Location update() { return null; } @@ -108,6 +108,14 @@ public Intent getInitIntent() { return LocationBackendService.this.getInitIntent(); } + public String getBackendName() { + return LocationBackendService.this.getBackendName(); + } + + public String getDescription() { + return LocationBackendService.this.getDescription(); + } + @Override public Intent getSettingsIntent() { return LocationBackendService.this.getSettingsIntent(); diff --git a/app-dejavu/.gitignore b/app-dejavu/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app-dejavu/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app-dejavu/build.gradle b/app-dejavu/build.gradle new file mode 100644 index 0000000..64bac99 --- /dev/null +++ b/app-dejavu/build.gradle @@ -0,0 +1,38 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 33 + defaultConfig { + minSdkVersion 14 + targetSdkVersion 33 + versionCode 32 + versionName "1.1.23" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + encoding "UTF-8" + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + packagingOptions { + exclude 'META-INF/*' + } +} + +dependencies { + implementation project(':api') + + testImplementation 'junit:junit:4.13' + testImplementation 'org.mockito:mockito-core:3.5.10' + + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.6.0' +} diff --git a/app-dejavu/proguard-rules.pro b/app-dejavu/proguard-rules.pro new file mode 100644 index 0000000..94a8e3b --- /dev/null +++ b/app-dejavu/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/tfitch/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app-dejavu/src/main/AndroidManifest.xml b/app-dejavu/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5cb5222 --- /dev/null +++ b/app-dejavu/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/BackendService.java b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/BackendService.java new file mode 100644 index 0000000..0df4ca5 --- /dev/null +++ b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/BackendService.java @@ -0,0 +1,1265 @@ +package org.microg.nlp.backend.dejavu; +/* + * DejaVu - A location provider backend for microG/UnifiedNlp + * + * Copyright (C) 2017 Tod Fitch + * + * 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. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * Created by tfitch on 8/27/17. + */ + +import android.Manifest; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.location.Location; +import android.location.LocationListener; +import android.net.wifi.ScanResult; +import android.net.wifi.WifiManager; +import android.os.Build; +import android.telephony.CellIdentityCdma; +import android.telephony.CellIdentityGsm; +import android.telephony.CellIdentityLte; +import android.telephony.CellIdentityWcdma; +import android.telephony.CellInfo; +import android.telephony.CellInfoCdma; +import android.telephony.CellInfoGsm; +import android.telephony.CellInfoLte; +import android.telephony.CellInfoWcdma; +import android.telephony.CellLocation; +import android.telephony.TelephonyManager; +import android.util.Log; + +import static android.Manifest.permission.ACCESS_COARSE_LOCATION; +import static android.Manifest.permission.ACCESS_FINE_LOCATION; +import static android.Manifest.permission.ACCESS_WIFI_STATE; +import static android.Manifest.permission.CHANGE_WIFI_STATE; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Queue; +import java.util.Set; + +import org.microg.nlp.api.LocationBackendService; +import org.microg.nlp.api.MPermissionHelperActivity; +import org.microg.nlp.backend.dejavu.database.BoundingBox; +import org.microg.nlp.backend.dejavu.database.Observation; +import org.microg.nlp.backend.dejavu.database.RfEmitter; +import org.microg.nlp.backend.dejavu.database.RfIdentification; +import org.microg.nlp.backend.dejavu.ui.MainActivity; + +import android.location.LocationManager; + +import static org.microg.nlp.backend.dejavu.LogToFile.appendLog; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; + +public class BackendService extends LocationBackendService implements LocationListener { + private static final String TAG = "DejaVu Backend"; + + public static final String LOCATION_PROVIDER = "DejaVu"; + private final boolean DEBUG = false; + + private static final + String[] myPerms = new String[]{ + ACCESS_WIFI_STATE, CHANGE_WIFI_STATE, + ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}; + + public static final double DEG_TO_METER = 111225.0; + public static final double METER_TO_DEG = 1.0 / DEG_TO_METER; + public static final double MIN_COS = 0.01; // for things that are dividing by the cosine + + // Define range of received signal strength to be used for all emitter types. + // Basically use the same range of values for LTE and WiFi as GSM defaults to. + public static final int MAXIMUM_ASU = 31; + public static final int MINIMUM_ASU = 1; + + // KPH -> Meters/millisec (KPH * 1000) / (60*60*1000) -> KPH/3600 + public static final float EXPECTED_SPEED = 120.0f / 3600; // 120KPH (74 MPH) + + private static final float NULL_ISLAND_DISTANCE = 1000; + private static Location nullIsland = new Location(BackendService.LOCATION_PROVIDER);; + + /** + * Process noise for lat and lon. + * + * We do not have an accelerometer, so process noise ought to be large enough + * to account for reasonable changes in vehicle speed. Assume 0 to 100 kph in + * 5 seconds (20kph/sec ~= 5.6 m/s**2 acceleration). Or the reverse, 6 m/s**2 + * is about 0-130 kph in 6 seconds + */ + private final static double GPS_COORDINATE_NOISE = 3.0; + private final static double POSITION_COORDINATE_NOISE = 6.0; + + private static BackendService instance; + private boolean gpsMonitorRunning = false; + private boolean wifiBroadcastReceiverRegistered = false; + private boolean permissionsOkay = true; + + // We use a threads for potentially slow operations. + private Thread mobileThread; + private Thread backgroundThread; + private boolean wifiScanInprogress; + + private TelephonyManager tm; + + // Stuff for scanning WiFi APs + private final static IntentFilter wifiBroadcastFilter = + new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); + + private WifiManager wm; + + private final BroadcastReceiver wifiBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + onWiFisChanged(); + } + }; + + private Location gpsLocation; // Filtered GPS (because GPS is so bad on Moto G4 Play) + + // + // Periodic process information. + // + // We keep a set of the WiFi APs we expected to see and ones we've seen and then + // periodically adjust the trust. Ones we've seen we increment, ones we expected + // to see but didn't we decrement. + // + private Set seenSet; + private Cache emitterCache; + + // + // Scanning and reporting are resource intensive operations, so we throttle + // them. Ideally the intervals should be multiples of one another. + // + // We are triggered by external events, so we really don't run periodically. + // So these numbers are the minimum time. Actual will be at least that based + // on when we get GPS locations and/or update requests from microG/UnifiedNlp. + // + private final static long REPORTING_INTERVAL = 2700; // in milliseconds + private final static long MOBILE_SCAN_INTERVAL = REPORTING_INTERVAL/2 - 100; // in milliseconds + private final static long WLAN_SCAN_INTERVAL = REPORTING_INTERVAL/3 - 100; // in milliseconds + + private long nextMobileScanTime; + private long nextWlanScanTime; + private long nextReportTime; + + // + // We want only a single background thread to do all the work but we have a couple + // of asynchronous inputs. So put everything into a work item queue. . . and have + // a single server pull and process the information. + // + private class WorkItem { + Collection observations; + Location loc; + long time; + + WorkItem(Collection o, Location l, long tm) { + observations = o; + loc = l; + time = tm; + } + } + private Queue workQueue = new ConcurrentLinkedQueue<>(); + + // + // Overrides of inherited methods + // + + private Context context; + + public BackendService(Context context) { + this.context = context; + } + + /** + * We are starting to run, get the resources we need to do our job. + */ + @Override + public void onOpen() { + Log.i(TAG, "onOpen() entry."); + appendLog(TAG, "onOpen() entry."); + nullIsland.setLatitude(0.0); + nullIsland.setLongitude(0.0); + instance = this; + nextReportTime = 0; + nextMobileScanTime = 0; + nextWlanScanTime = 0; + wifiBroadcastReceiverRegistered = false; + wifiScanInprogress = false; + + if (emitterCache == null) + emitterCache = new Cache(context); + + permissionsOkay = true; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // Check our needed permissions, don't run unless we can. + appendLog(TAG, "onOpen():myPerms:" + myPerms); + for (String s : myPerms) { + permissionsOkay &= (context.checkSelfPermission(s) == PackageManager.PERMISSION_GRANTED); + appendLog(TAG, "onOpen():permissionsOkay:" + permissionsOkay); + } + } + appendLog(TAG, "onOpen():permissionsOkay final:" + permissionsOkay); + if (permissionsOkay) { + setgpsMonitorRunning(true); + context.registerReceiver(wifiBroadcastReceiver, wifiBroadcastFilter); + wifiBroadcastReceiverRegistered = true; + } else { + Log.i(TAG, "onOpen() - Permissions not granted, soft fail."); + appendLog(TAG, "onOpen() - Permissions not granted, soft fail."); + } + } + + /** + * Closing down, release our dynamic resources. + */ + @Override + protected synchronized void onClose() { + Log.i(TAG, "onClose()"); + appendLog(TAG, "onClose()"); + if (wifiBroadcastReceiverRegistered) { + context.unregisterReceiver(wifiBroadcastReceiver); + } + setgpsMonitorRunning(false); + + if (emitterCache != null) { + emitterCache.close(); + emitterCache = null; + } + + if (instance == this) { + instance = null; + } + } + + public String getBackendName() { + return "org.microg.nlp.backend.dejavu.BackendService"; + } + + public String getDescription() { + return "Dejavu backend serivce"; + } + + /** + * Called by MicroG/UnifiedNlp when our backend is enabled. We return a list of + * the Android permissions we need but have not (yet) been granted. MicroG will + * handle putting up the dialog boxes, etc. to get our permissions granted. + * + * @return An intent with the list of permissions we need to run. + */ + @Override + public Intent getInitIntent() { + appendLog(TAG, "getInitIntent()"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // Build list of permissions we need but have not been granted + List perms = new LinkedList<>(); + appendLog(TAG, "getInitIntent():myPerms:" + myPerms); + for (String s : myPerms) { + if (context.checkSelfPermission(s) != PackageManager.PERMISSION_GRANTED) { + perms.add(s); + appendLog(TAG, "getInitIntent():perms.add:" + s); + } + } + + // Send the list of permissions we need to UnifiedNlp so it can ask for + // them to be granted. + appendLog(TAG, "getInitIntent():perms.isEmpty():" + perms.isEmpty()); + if (perms.isEmpty()) + return null; + Intent intent = new Intent(context, MPermissionHelperActivity.class); + intent.putExtra(MPermissionHelperActivity.EXTRA_PERMISSIONS, perms.toArray(new String[perms.size()])); + appendLog(TAG, "getInitIntent():intent:" + intent); + return intent; + } + appendLog(TAG, "getInitIntent():intent from super"); + return super.getInitIntent(); + } + + @Override + public Intent getSettingsIntent() { + Intent intent = new Intent(context, MainActivity.class); + return intent; + } + + /** + * Called by microG/UnifiedNlp when it wants a position update. We return a null indicating + * we don't have a current position but treat it as a good time to kick off a scan of all + * our RF sensors. + * + * @return Always null. + */ + @Override + public Location update() { + //Log.i(TAG, "update() entry."); + appendLog(TAG, "update() entry."); + if (permissionsOkay) { + scanAllSensors(); + } else { + Log.i(TAG, "update() - Permissions not granted, soft fail."); + appendLog(TAG, "update() - Permissions not granted, soft fail."); + } + return null; + } + + // + // Other public methods + // + + /** + * Called by Android when a GPS location reports becomes available. + * + * @param location The current GPS position estimate + */ + public void onLocationChanged(Location location) { + //Log.i(TAG, "instanceGpsLocationUpdated() entry."); + appendLog(TAG, "instanceGpsLocationUpdated() entry:" + location); + if ((instance != null) && (LocationManager.GPS_PROVIDER.equals(location.getProvider()))) { + instance.onGpsChanged(location); + } + } + + /** + * Check if location too close to null island to be real + * + * @param loc The location to be checked + * @return boolean True if away from lat,lon of 0,0 + */ + public static boolean notNullIsland(Location loc) { + return (nullIsland.distanceTo(loc) > NULL_ISLAND_DISTANCE); + } + + // + // Private methods + // + + /** + * Called when we have a new GPS position report from Android. We update our local + * Kalman filter (our best guess on GPS reported position) and since our location is + * pretty current it is a good time to kick of a scan of RF sensors. + * + * @param updt The current GPS reported location + */ + private void onGpsChanged(Location updt) { + synchronized (this) { + if (permissionsOkay) { + if (notNullIsland(updt) && (LocationManager.GPS_PROVIDER.equals(updt.getProvider()))) { + //Log.i(TAG, "onGpsChanged() entry."); + appendLog(TAG, "onGpsChanged() entry:" + updt); + gpsLocation = updt; //new Kalman(updt, GPS_COORDINATE_NOISE); + scanAllSensors(); + } + } else { + Log.i(TAG, "onGpsChanged() - Permissions not granted, soft fail."); + appendLog(TAG, "onGpsChanged() - Permissions not granted, soft fail."); + } + } + } + + /** + * Kick off new scans for all the sensor types we know about. Typically scans + * should occur asynchronously so we don't hang up our caller's thread. + */ + private void scanAllSensors() { + synchronized (this) { + if (emitterCache == null) { + Log.i(TAG, "scanAllSensors() - emitterCache is null?!?"); + appendLog(TAG, "scanAllSensors() - emitterCache is null?!?"); + return; + } + startWiFiScan(); + startMobileScan(); + } + } + + /** + * Ask Android's WiFi manager to scan for access points (APs). When done the onWiFisChanged() + * method will be called by Android. + */ + private void startWiFiScan() { + // Throttle scanning for WiFi APs. In open terrain an AP could cover a kilometer. + // Even in a vehicle moving at highway speeds it can take several seconds to traverse + // the coverage area, no need to waste phone resources scanning too rapidly. + long currentProcessTime = System.currentTimeMillis(); + if (currentProcessTime < nextWlanScanTime) + return; + nextWlanScanTime = currentProcessTime + WLAN_SCAN_INTERVAL; + + if (wm == null) { + wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + } + if ((wm != null) && !wifiScanInprogress) { + if (wm.isWifiEnabled() || + ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) && wm.isScanAlwaysAvailable())) { + Log.i(TAG,"startWiFiScan() - Starting WiFi collection."); + appendLog(TAG, "startWiFiScan() - Starting WiFi collection."); + wifiScanInprogress = true; + wm.startScan(); + } + } + } + + /** + * Start a separate thread to scan for mobile (cell) towers. This can take some time so + * we won't do it in the caller's thread. + */ + private synchronized void startMobileScan() { + // Throttle scanning for mobile towers. Generally each tower covers a significant amount + // of terrain so even if we are moving fairly rapidly we should remain in a single tower's + // coverage area for several seconds. No need to sample more ofen than that and we save + // resources on the phone. + + long currentProcessTime = System.currentTimeMillis(); + if (currentProcessTime < nextMobileScanTime) + return; + nextMobileScanTime = currentProcessTime + MOBILE_SCAN_INTERVAL; + + // Scanning towers takes some time, so do it in a separate thread. + if (mobileThread != null) { + Log.i(TAG,"startMobileScan() - Thread exists."); + appendLog(TAG, "startMobileScan() - Thread exists."); + return; + } + //Log.i(TAG,"startMobileScan() - Starting mobile signal scan thread."); + appendLog(TAG, "startMobileScan() - Starting mobile signal scan thread."); + mobileThread = new Thread(new Runnable() { + @Override + public void run() { + scanMobile(); + mobileThread = null; + } + }); + mobileThread.start(); + } + + /** + * Scan for the mobile (cell) towers the phone sees. If we see any, then add them + * to the queue for background processing. + */ + private void scanMobile() { + Log.i(TAG, "scanMobile() - calling getMobileTowers()."); + appendLog(TAG, "scanMobile() - calling getMobileTowers()."); + Collection observations = getMobileTowers(); + + if (observations.size() > 0) { + Log.i(TAG,"scanMobile() " + observations.size() + " records to be queued for processing."); + appendLog(TAG, "scanMobile() " + observations.size() + " records to be queued for processing."); + queueForProcessing(observations, System.currentTimeMillis()); + } + } + + /** + * Get the set of mobile (cell) towers that Android claims the phone can see. + * we use the current API but fall back to deprecated methods if we get a null + * or empty result from the current API. + * + * @return A set of mobile tower observations + */ + private Set getMobileTowers() { + if (tm == null) { + tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + } + + Set observations = new HashSet<>(); + + CellLocation cellLocation = null; + try { + cellLocation = tm.getCellLocation(); + } catch (SecurityException securityException) { + appendLog(TAG, "SecurityException when getCellLocation is called ", securityException); + } + + appendLog(TAG, "getCells():cellLocation:" + cellLocation); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + appendLog(TAG, "getAllCellInfo is not available (requires API 17)"); + return observations; + } + + // Try most recent API to get all cell information + List allCells = null; + try { + if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + allCells = tm.getAllCellInfo(); + } else { + appendLog(TAG, "ACCESS_FINE_LOCATION is not granted"); + } + } catch (NoSuchMethodError e) { + allCells = null; + Log.i(TAG, "getMobileTowers(): no such method: getAllCellInfo()."); + appendLog(TAG, "getMobileTowers(): no such method: getAllCellInfo()."); + } + observations = processCellInfos(allCells); + requestCellInfoUpdateForMobileTowers(); + //Log.i(TAG, "getMobileTowers(): Observations: " + observations.toString()); + appendLog(TAG, "getMobileTowers(): Observations: " + observations.toString()); + return observations; + } + + private String calculateUnregistered(List allCells) { + StringBuilder idStr = new StringBuilder(); + idStr.append("Unregistered"); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + return idStr.toString(); + } + for (CellInfo inputCellInfo : allCells) { + if (inputCellInfo instanceof CellInfoLte) { + CellInfoLte info = (CellInfoLte) inputCellInfo; + CellIdentityLte id = info.getCellIdentity(); + + idStr.append("/LTE"); + if (id.getCi() != Integer.MAX_VALUE) { + idStr.append("/").append(id.getCi()); + } + if (id.getMcc() != Integer.MAX_VALUE) { + idStr.append("/").append(id.getMcc()); + } + if (id.getMnc() != Integer.MAX_VALUE) { + idStr.append("/").append(id.getMnc()); + } + if (id.getPci() != Integer.MAX_VALUE) { + idStr.append("/").append(id.getPci()); + } + if (id.getTac() != Integer.MAX_VALUE) { + idStr.append("/").append(id.getTac()); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (id.getEarfcn() != Integer.MAX_VALUE) { + idStr.append("/").append(id.getEarfcn()); + } + } + } else if (inputCellInfo instanceof CellInfoGsm) { + CellInfoGsm info = (CellInfoGsm) inputCellInfo; + CellIdentityGsm id = info.getCellIdentity(); + + idStr.append("/GSM"); + if (id.getCid() != Integer.MAX_VALUE) { + idStr.append("/").append(id.getCid()); + } + if (id.getMcc() != Integer.MAX_VALUE) { + idStr.append("/").append(id.getMcc()); + } + if (id.getMnc() != Integer.MAX_VALUE) { + idStr.append("/").append(id.getMnc()); + } + if (id.getLac() != Integer.MAX_VALUE) { + idStr.append("/").append(id.getLac()); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (id.getArfcn() != Integer.MAX_VALUE) { + idStr.append("/").append(id.getArfcn()); + } + } + } else if (inputCellInfo instanceof CellInfoWcdma) { + CellInfoWcdma info = (CellInfoWcdma) inputCellInfo; + CellIdentityWcdma id = info.getCellIdentity(); + + idStr.append("/WCDMA"); + if (id.getCid() != Integer.MAX_VALUE) { + idStr.append("/").append(id.getCid()); + } + if (id.getMcc() != Integer.MAX_VALUE) { + idStr.append("/").append(id.getMcc()); + } + if (id.getMnc() != Integer.MAX_VALUE) { + idStr.append("/").append(id.getMnc()); + } + if (id.getLac() != Integer.MAX_VALUE) { + idStr.append("/").append(id.getLac()); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (id.getUarfcn() != Integer.MAX_VALUE) { + idStr.append("/").append(id.getUarfcn()); + } + } + } else if (inputCellInfo instanceof CellInfoCdma) { + CellInfoCdma info = (CellInfoCdma) inputCellInfo; + CellIdentityCdma id = info.getCellIdentity(); + + idStr.append("/CDMA"); + if (id.getNetworkId() != Integer.MAX_VALUE) { + idStr.append("/").append(id.getNetworkId()); + } + if (id.getSystemId() != Integer.MAX_VALUE) { + idStr.append("/").append(id.getSystemId()); + } + if (id.getBasestationId() != Integer.MAX_VALUE) { + idStr.append("/").append(id.getBasestationId()); + } + } + } + return idStr.toString(); + } + + private Set processCellInfos(List allCells) { + Set observations = new HashSet<>(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + appendLog(TAG, "getAllCellInfo is not available (requires API 17)"); + return observations; + } + if ((allCells != null) && !allCells.isEmpty()) { + Log.i(TAG, "getMobileTowers(): getAllCellInfo() returned " + allCells.size() + "records."); + appendLog(TAG, "getMobileTowers(): getAllCellInfo() returned " + allCells.size() + "records."); + + String registeredCellIdInfo = null; + + for (CellInfo inputCellInfo : allCells) { + if (inputCellInfo.isRegistered()) { + if (inputCellInfo instanceof CellInfoLte) { + CellInfoLte info = (CellInfoLte) inputCellInfo; + CellIdentityLte id = info.getCellIdentity(); + registeredCellIdInfo = "LTE" + "/" + id.getMcc() + "/" + + id.getMnc() + "/" + id.getCi() + "/" + + id.getPci() + "/" + id.getTac(); + } else if (inputCellInfo instanceof CellInfoGsm) { + CellInfoGsm info = (CellInfoGsm) inputCellInfo; + CellIdentityGsm id = info.getCellIdentity(); + + registeredCellIdInfo = "GSM" + "/" + id.getMcc() + "/" + + id.getMnc() + "/" + id.getLac() + "/" + + id.getCid(); + } else if (inputCellInfo instanceof CellInfoWcdma) { + CellInfoWcdma info = (CellInfoWcdma) inputCellInfo; + CellIdentityWcdma id = info.getCellIdentity(); + + registeredCellIdInfo = "WCDMA" + "/" + id.getMcc() + "/" + + id.getMnc() + "/" + id.getLac() + "/" + + id.getCid(); + } else if (inputCellInfo instanceof CellInfoCdma) { + CellInfoCdma info = (CellInfoCdma) inputCellInfo; + CellIdentityCdma id = info.getCellIdentity(); + + registeredCellIdInfo = "CDMA" + "/" + id.getNetworkId() + "/" + + id.getSystemId() + "/" + id.getBasestationId(); + } + } + } + // no registered network, roaming + if (registeredCellIdInfo == null) { + registeredCellIdInfo = calculateUnregistered(allCells); + } + + for (CellInfo inputCellInfo : allCells) { + Log.i(TAG, "getMobileTowers(): inputCellInfo: " + inputCellInfo.toString()); + appendLog(TAG, "getMobileTowers(): inputCellInfo: " + inputCellInfo.toString()); + + if (inputCellInfo instanceof CellInfoLte) { + CellInfoLte info = (CellInfoLte) inputCellInfo; + CellIdentityLte id = info.getCellIdentity(); + + // CellIdentityLte accessors all state Integer.MAX_VALUE is returned for unknown values. + String idStr = null; + if ((id.getCi() != Integer.MAX_VALUE) && (id.getPci() != Integer.MAX_VALUE) && + (id.getTac() != Integer.MAX_VALUE)) { + // Log.i(TAG, "getMobileTowers(): LTE tower: " + info.toString()); + appendLog(TAG, "getMobileTowers(): LTE tower: " + info.toString()); + idStr = "LTE" + "/" + id.getMcc() + "/" + + id.getMnc() + "/" + id.getCi() + "/" + + id.getPci() + "/" + id.getTac(); + } else if (registeredCellIdInfo != null) { + idStr = registeredCellIdInfo; + boolean infoAdded = false; + if (id.getPci() != Integer.MAX_VALUE) { + idStr += "/" + id.getPci(); + infoAdded = true; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (id.getEarfcn() != Integer.MAX_VALUE) { + idStr += "/" + id.getEarfcn(); + infoAdded = true; + } + } + if (!infoAdded) { + idStr = null; + } else { + appendLog(TAG, "getMobileTowers(): LTE tower with additional info: " + idStr); + } + } + + if (idStr != null) { + int asu = (info.getCellSignalStrength().getAsuLevel() * MAXIMUM_ASU) / 97; + + Observation o = new Observation(idStr, RfEmitter.EmitterType.MOBILE); + o.setAsu(asu); + observations.add(o); + } else { + appendLog(TAG, "getMobileTowers(): LTE Cell Identity has unknown values: " + id.toString()); + if (DEBUG) + Log.i(TAG, "getMobileTowers(): LTE Cell Identity has unknown values: " + id.toString()); + } + } else if (inputCellInfo instanceof CellInfoGsm) { + CellInfoGsm info = (CellInfoGsm) inputCellInfo; + CellIdentityGsm id = info.getCellIdentity(); + + String idStr = null; + // CellIdentityGsm accessors all state Integer.MAX_VALUE is returned for unknown values. + if ((id.getLac() != Integer.MAX_VALUE) && (id.getCid() != Integer.MAX_VALUE)) { + // Log.i(TAG, "getMobileTowers(): GSM tower: " + info.toString()); + appendLog(TAG, "getMobileTowers(): GSM tower: " + info.toString()); + idStr = "GSM" + "/" + id.getMcc() + "/" + + id.getMnc() + "/" + id.getLac() + "/" + + id.getCid(); + } else if (registeredCellIdInfo != null) { + idStr = registeredCellIdInfo; + boolean infoAdded = false; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (id.getArfcn() != Integer.MAX_VALUE) { + idStr += "/" + id.getArfcn(); + infoAdded = true; + } + } + if (!infoAdded) { + idStr = null; + } else { + appendLog(TAG, "getMobileTowers(): GSM tower with additional info: " + idStr); + } + } + + if (idStr != null) { + int asu = info.getCellSignalStrength().getAsuLevel(); + Observation o = new Observation(idStr, RfEmitter.EmitterType.MOBILE); + o.setAsu(asu); + observations.add(o); + } else { + appendLog(TAG, "getMobileTowers(): GSM Cell Identity has unknown values: " + id.toString()); + if (DEBUG) + Log.i(TAG, "getMobileTowers(): GSM Cell Identity has unknown values: " + id.toString()); + } + } else if (inputCellInfo instanceof CellInfoWcdma) { + CellInfoWcdma info = (CellInfoWcdma) inputCellInfo; + CellIdentityWcdma id = info.getCellIdentity(); + String idStr = null; + // CellIdentityWcdma accessors all state Integer.MAX_VALUE is returned for unknown values. + if ((id.getLac() != Integer.MAX_VALUE) && (id.getCid() != Integer.MAX_VALUE)) { + // Log.i(TAG, "getMobileTowers(): WCDMA tower: " + info.toString()); + appendLog(TAG, "getMobileTowers(): WCDMA tower: " + info.toString()); + idStr = "WCDMA" + "/" + id.getMcc() + "/" + + id.getMnc() + "/" + id.getLac() + "/" + + id.getCid(); + } else if (registeredCellIdInfo != null) { + idStr = registeredCellIdInfo; + boolean infoAdded = false; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (id.getUarfcn() != Integer.MAX_VALUE) { + idStr += "/" + id.getUarfcn(); + infoAdded = true; + } + } + if (!infoAdded) { + idStr = null; + } else { + appendLog(TAG, "getMobileTowers(): WCDMA tower with additional info: " + idStr); + } + } + + if (idStr != null) { + int asu = info.getCellSignalStrength().getAsuLevel(); + Observation o = new Observation(idStr, RfEmitter.EmitterType.MOBILE); + o.setAsu(asu); + observations.add(o); + } else { + appendLog(TAG, "getMobileTowers(): WCDMA Cell Identity has unknown values: " + id.toString()); + if (DEBUG) + Log.i(TAG, "getMobileTowers(): WCDMA Cell Identity has unknown values: " + id.toString()); + } + } else if (inputCellInfo instanceof CellInfoCdma) { + CellInfoCdma info = (CellInfoCdma) inputCellInfo; + CellIdentityCdma id = info.getCellIdentity(); + String idStr = null; + // CellIdentityCdma accessors all state Integer.MAX_VALUE is returned for unknown values. + if ((id.getNetworkId() != Integer.MAX_VALUE) && (id.getSystemId() != Integer.MAX_VALUE) && + (id.getBasestationId() != Integer.MAX_VALUE)) { + // Log.i(TAG, "getMobileTowers(): CDMA tower: " + info.toString()); + appendLog(TAG, "getMobileTowers(): CDMA tower: " + info.toString()); + idStr = "CDMA" + "/" + id.getNetworkId() + "/" + + id.getSystemId() + "/" + id.getBasestationId(); + } + + if (idStr != null) { + int asu = info.getCellSignalStrength().getAsuLevel(); + Observation o = new Observation(idStr, RfEmitter.EmitterType.MOBILE); + o.setAsu(asu); + observations.add(o); + } else { + appendLog(TAG, "getMobileTowers(): CDMA Cell Identity has unknown values: " + id.toString()); + if (DEBUG) + Log.i(TAG, "getMobileTowers(): CDMA Cell Identity has unknown values: " + id.toString()); + } + } else { + appendLog(TAG, "getMobileTowers(): Unsupported Cell type: " + inputCellInfo.toString()); + Log.i(TAG, "getMobileTowers(): Unsupported Cell type: " + inputCellInfo.toString()); + } + } + } + return observations; + } + + private void requestCellInfoUpdateForMobileTowers() { + + if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + tm.requestCellInfoUpdate(context.getMainExecutor(), new TelephonyManager.CellInfoCallback() { + @Override + public void onCellInfo(@NonNull List cellInfo) { + queueForProcessing(processCellInfos(cellInfo), System.currentTimeMillis()); + } + }); + } else { + appendLog(TAG, "getAllCellInfo is not available (requires API 17)"); + } + } + } + + private static final int GPS_SAMPLE_TIME = 0; + private static final float GPS_SAMPLE_DISTANCE = 0; + + /** + * Control whether or not we are listening for position reports from other sources. + * The only one we care about is the GPS, thus the name. + * + * @param enable A boolean value, true enables monitoring. + */ + private void setgpsMonitorRunning(boolean enable) { + // Log.i(TAG,"setgpsMonitorRunning(" + enable + ")"); + appendLog(TAG, "setgpsMonitorRunning(" + enable + ")"); + LocationManager lm = (LocationManager) context.getApplicationContext().getSystemService(Context.LOCATION_SERVICE); + if(enable != gpsMonitorRunning) { + if (enable) { + //bindService(new Intent(this, GpsMonitor.class), mConnection, Context.BIND_AUTO_CREATE); + if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + lm.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, + GPS_SAMPLE_TIME, + GPS_SAMPLE_DISTANCE, + this); + } + } else { + //unbindService(mConnection); + try { + lm.removeUpdates(this); + } catch (SecurityException ex) { + // ignore + } + } + gpsMonitorRunning = enable; + } + } + + /** + * Call back method entered when Android has completed a scan for WiFi emitters in + * the area. + */ + private synchronized void onWiFisChanged() { + if ((wm != null) && (emitterCache != null)) { + Set observations = new HashSet<>(); + if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + List scanResults = wm.getScanResults(); + for (ScanResult sr : scanResults) { + String bssid = sr.BSSID.toLowerCase(Locale.US).replace(".", ":"); + RfEmitter.EmitterType rftype = RfEmitter.EmitterType.WLAN_24GHZ; + if (is5GHz(sr)) + rftype = RfEmitter.EmitterType.WLAN_5GHZ; + Log.i(TAG,"rfType="+rftype.toString()+", ScanResult="+sr.toString()); + if (bssid != null) { + Observation o = new Observation(bssid, rftype); + + o.setAsu(WifiManager.calculateSignalLevel(sr.level, MAXIMUM_ASU)); + o.setNote(sr.SSID); + observations.add(o); + } + } + } else { + appendLog(TAG, "ACCESS_FINE_LOCATION is not granted"); + } + if (!observations.isEmpty()) { + Log.i(TAG, "onWiFisChanged(): Observations: " + observations.toString()); + appendLog(TAG, "onWiFisChanged(): Observations: " + observations.toString()); + queueForProcessing(observations, System.currentTimeMillis()); + } + } + wifiScanInprogress = false; + } + + /** + * This seems like it ought to be in ScanResult but I get an unidentified error + * @param sr Result from a WLAN/WiFi scan + * @return True if in the 5GHZ range + */ + static boolean is5GHz(ScanResult sr) { + int freq = sr.frequency; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (sr.channelWidth != ScanResult.CHANNEL_WIDTH_20MHZ) + freq = sr.centerFreq0; + } + return freq > 2500; + } + + /** + * Add a collection of observations to our background thread's work queue. If + * no thread currently exists, start one. + * + * @param observations A set of RF emitter observations (all must be of the same type) + * @param timeMs The time the observations were made. + */ + private synchronized void queueForProcessing(Collection observations, + long timeMs) { + WorkItem work = new WorkItem(observations, gpsLocation, timeMs); + workQueue.offer(work); + + if (backgroundThread != null) { + // Log.i(TAG,"queueForProcessing() - Thread exists."); + appendLog(TAG, "queueForProcessing() - Thread exists."); + return; + } + + backgroundThread = new Thread(new Runnable() { + @Override + public void run() { + WorkItem myWork = workQueue.poll(); + while (myWork != null) { + backgroundProcessing(myWork); + myWork = workQueue.poll(); + } + backgroundThread = null; + } + }); + backgroundThread.start(); + } + + // + // Generic private methods + // + + /** + * Process a group of observations. Process in this context means + * 1. Add the emitters to the set of emitters we have seen in this processing period. + * 2. If the GPS is accurate enough, update our coverage estimates for the emitters. + * 3. If the GPS is accurate enough, update a list of emitters we think we should have seen. + * 3. Compute a position based on the current observations. + * 4. If our collection period is over, report our position to microG/UnifiedNlp and + * synchonize our information with the flash based database. + * + * @param myWork + */ + private synchronized void backgroundProcessing(WorkItem myWork) { + if (emitterCache == null) + return; + + if (seenSet == null) + seenSet = new HashSet<>(); + + Collection emitters = new HashSet<>(); + + // Remember all the emitters we've seen during this processing period + // and build a set of emitter objects for each RF emitter in the + // observation set. + + for (Observation o : myWork.observations) { + seenSet.add(o.getIdent()); + RfEmitter e = emitterCache.get(o.getIdent()); + if (e != null) { + e.setLastObservation(o); + emitters.add(e); + } + } + + // Update emitter coverage based on GPS as needed and get the set of locations + // the emitters are known to be seen at. + + updateEmitters( emitters, myWork.loc, myWork.time); + + // Check for the end of our collection period. If we are in a new period + // then finish off the processing for the previous period. + long currentProcessTime = System.currentTimeMillis(); + if (currentProcessTime >= nextReportTime) { + nextReportTime = currentProcessTime + REPORTING_INTERVAL; + endOfPeriodProcessing(); + } + } + + /** + * Update the coverage estimates for the emitters we have just gotten observations for. + * + * @param emitters The emitters we have just observed + * @param gps The GPS position at the time the observations were collected. + * @param curTime The time the observations were collected + */ + private synchronized void updateEmitters(Collection emitters, Location gps, long curTime) { + + if (emitterCache == null) { + Log.i(TAG,"updateEmitters() - emitterCache is null?!?"); + appendLog(TAG, "updateEmitters() - emitterCache is null?!?"); + emitterCache = new Cache(context); + } + + for (RfEmitter emitter : emitters) { + emitter.updateLocation(gps); + } + } + + /** + * Get coverage estimates for a list of emitter IDs. Locations are marked with the + * time of last update, etc. + * + * @param rfids IDs of the emitters desired + * @return A list of the coverage areas for the emitters + */ + private List getRfLocations(Collection rfids) { + List locations = new LinkedList<>(); + for (RfIdentification id : rfids) { + appendLog(TAG, "getRfLocations:rfid:" + id.toString()); + RfEmitter e = emitterCache.get(id); + if (e != null) { + Location l = e.getLocation(); + if (l != null) { + appendLog(TAG, "getRfLocations:rfEmiterFromCache:" + e + ", location:" + l); + locations.add(l); + } + } + } + return locations; + } + + /** + * Compute our current location using a weighted average algorithm. We also keep + * track of the types of emitters we have seen for the end of period processing. + * + * For any given reporting interval, we will only use an emitter once, so we keep + * a set of used emitters. + * + * @param locations The set of coverage information for the current observations + */ + private Location computePostion(Collection locations) { + if (locations == null) + return null; + + WeightedAverage weightedAverage = new WeightedAverage(); + for (Location l : locations) { + weightedAverage.add(l); + } + return weightedAverage.result(); + } + + /** + * + * The collector service attempts to detect and not report moved/moving emitters. + * But it (and thus our database) can't be perfect. This routine looks at all the + * emitters and returns the largest subset (group) that are within a reasonable + * distance of one another. + * + * The hope is that a single moved/moving emitters that is seen now but whose + * location was detected miles away can be excluded from the set of APs + * we use to determine where the phone is at this moment. + * + * We do this by creating collections of emitters where all the emitters in a group + * are within a plausible distance of one another. A single emitters may end up + * in multiple groups. When done, we return the largest group. + * + * If we are at the extreme limit of possible coverage (movedThreshold) + * from two emitters then those emitters could be a distance of 2*movedThreshold apart. + * So we will group the emitters based on that large distance. + * + * @param locations A collection of the coverages for the current observation set + * @return The largest set of coverages found within the raw observations. That is + * the most believable set of coverage areas. + */ + private Set culledEmitters(Collection locations) { + Set> locationGroups = divideInGroups(locations); + + List> clsList = new ArrayList<>(locationGroups); + Collections.sort(clsList, new Comparator>() { + @Override + public int compare(Set lhs, Set rhs) { + return rhs.size() - lhs.size(); + } + }); + + if (!clsList.isEmpty()) { + Set rslt = clsList.get(0); + + // Determine minimum count for a valid group of emitters. + // The RfEmitter class will have put the min count into the location + // it provided. + Long reqdCount = 99999L; // Some impossibly big number + for (Location l : rslt) { + reqdCount = Math.min(l.getExtras().getLong(RfEmitter.LOC_MIN_COUNT,9999L),reqdCount); + } + //Log.i(TAG,"culledEmitters() reqdCount="+reqdCount+", size="+rslt.size()); + appendLog(TAG, "culledEmitters() reqdCount="+reqdCount+", size="+rslt.size()); + if (rslt.size() >= reqdCount) + return rslt; + } + return null; + } + + /** + * Build a set of sets (or groups) each outer set member is a set of coverage of + * reasonably near RF emitters. Basically we are grouping the raw observations + * into clumps based on how believably close together they are. An outlying emitter + * will likely be put into its own group. Our caller will take the largest set as + * the most believable group of observations to use to compute a position. + * + * @param locations A set of RF emitter coverage records + * @return A set of coverage sets. + */ + private Set> divideInGroups(Collection locations) { + + Set> bins = new HashSet<>(); + + // Create a bins + for (Location location : locations) { + Set locGroup = new HashSet<>(); + locGroup.add(location); + bins.add(locGroup); + } + + for (Location location : locations) { + for (Set locGroup : bins) { + if (locationCompatibleWithGroup(location, locGroup)) { + locGroup.add(location); + } + } + } + return bins; + } + + /** + * Check to see if the coverage area (location) of an RF emitter is close + * enough to others in a group that we can believably add it to the group. + * @param location The coverage area of the candidate emitter + * @param locGroup The coverage areas of the emitters already in the group + * @return True if location is close to others in group + */ + private boolean locationCompatibleWithGroup(Location location, + Set locGroup) { + + // If the location is within range of all current members of the + // group, then we are compatible. + for (Location other : locGroup) { + double testDistance = (location.distanceTo(other) - + location.getAccuracy() - + other.getAccuracy()); + + if (testDistance > 0.0) { + //Log.i(TAG,"locationCompatibleWithGroup(): "+testDistance); + appendLog(TAG, "locationCompatibleWithGroup(): "+testDistance); + return false; + } + } + return true; + } + + /** + * We bulk up operations to reduce writing to flash memory. And there really isn't + * much need to report location to microG/UnifiedNlp more often than once every three + * or four seconds. Another reason is that we can average more samples into each + * report so there is a chance that our position computation is more accurate. + */ + private void endOfPeriodProcessing() { + + //Log.i(TAG,"endOfPeriodProcessing() - Starting new process period."); + appendLog(TAG, "endOfPeriodProcessing() - Starting new process period."); + + // Estimate location using weighted average of the most recent + // observations from the set of RF emitters we have seen. We cull + // the locations based on distance from each other to reduce the + // chance that a moved/moving emitter will be used in the computation. + + Collection locations = culledEmitters(getRfLocations(seenSet)); + Location weightedAverageLocation = computePostion(locations); + if ((weightedAverageLocation != null) && notNullIsland(weightedAverageLocation)) { + //Log.i(TAG, "endOfPeriodProcessing(): " + weightedAverageLocation.toString()); + appendLog(TAG, "endOfPeriodProcessing(): " + weightedAverageLocation.toString()); + report(weightedAverageLocation); + } + + // Increment the trust of the emitters we've seen and decrement the trust + // of the emitters we expected to see but didn't. + + if (seenSet != null) { + for (RfIdentification id : seenSet) { + if (id != null) { + RfEmitter e = emitterCache.get(id); + if (e != null) + e.incrementTrust(); + } + } + } + + // If we are dealing with very movable emitters, then try to detect ones that + // have moved out of the area. We do that by collecting the set of emitters + // that we expected to see in this area based on the GPS and our own location + // computation. + + Set expectedSet = new HashSet<>(); + if (weightedAverageLocation != null) { + emitterCache.sync(); // getExpected() ends bypassing the cache, so sync first + + for (RfEmitter.EmitterType etype : RfEmitter.EmitterType.values()) { + expectedSet.addAll(getExpected(weightedAverageLocation, etype)); + } + if (gpsLocation != null) { + for (RfEmitter.EmitterType etype : RfEmitter.EmitterType.values()) { + expectedSet.addAll(getExpected(gpsLocation, etype)); + } + } + } + + for (RfIdentification u : expectedSet) { + if (!seenSet.contains(u)) { + RfEmitter e = emitterCache.get(u); + if (e != null) { + e.decrementTrust(); + } + } + } + + // Sync all of our changes to the on flash database and reset the RF emitters we've seen. + + emitterCache.sync(); + seenSet = new HashSet<>(); + } + + /** + * Add all the RF emitters of the specified type within the specified bounding + * box to the set of emitters we expect to see. This is used to age out emitters + * that may have changed locations (or gone off the air). When aged out we + * can remove them from our database. + * + * @param loc The location we think we are at. + * @param rfType The type of RF emitters we expect to see within the bounding + * box. + * @return A set of IDs for the RF emitters we should expect in this location. + */ + private Set getExpected(Location loc, RfEmitter.EmitterType rfType) { + RfEmitter.RfCharacteristics rfChar = RfEmitter.getRfCharacteristics(rfType); + if ((loc == null) || (loc.getAccuracy() > rfChar.typicalRange)) + return new HashSet<>(); + BoundingBox bb = new BoundingBox(loc.getLatitude(), loc.getLongitude(), rfChar.typicalRange); + return emitterCache.getEmitters(rfType, bb); + } +} + diff --git a/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/Cache.java b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/Cache.java new file mode 100644 index 0000000..4401009 --- /dev/null +++ b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/Cache.java @@ -0,0 +1,188 @@ +package org.microg.nlp.backend.dejavu; +/* + * DejaVu - A location provider backend for microG/UnifiedNlp + * + * Copyright (C) 2017 Tod Fitch + * + * 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. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * Created by tfitch on 10/4/17. + */ + +import android.content.Context; +import android.util.Log; + +import org.microg.nlp.backend.dejavu.database.BoundingBox; +import org.microg.nlp.backend.dejavu.database.Database; +import org.microg.nlp.backend.dejavu.database.RfEmitter; +import org.microg.nlp.backend.dejavu.database.RfIdentification; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * All access to the database is done through this cache: + * + * When a RF emitter is seen a get() call is made to the cache. If we have a cache hit + * the information is directly returned. If we have a cache miss we create a new record + * and populate it with either default information or information from the flash based + * database (if it exists in the database). + * + * Periodically we are asked to sync any new or changed RF emitter information to the + * database. When that occurs we group all the changes in one database transaction for + * speed. + * + * If an emitter has not been used for a while we will remove it from the cache (only + * immediately after a sync() operation so the record will be clean). If the cache grows + * too large we will clear it to conservery RAM (this should never happen). Again the + * clear operation will only occur after a sync() so any dirty records will be flushed + * to the database. + * + * Operations on the cache are thread safe. However the underlying RF emitter objects + * that are returned by the cache are not thread safe. So all work on them should be + * performed either in a single thread or with synchronization. + */ +class Cache { + private static final int MAX_WORKING_SET_SIZE = 200; + private static final int MAX_AGE = 30; + + private static final String TAG="DejaVu Cache"; + + /** + * Map (since they all must have different identifications) of + * all the emitters we are working with. + */ + private final Map workingSet = new HashMap<>(); + private Database db; + + Cache(Context context) { + db = new Database(context); + } + + /** + * Release all resources associated with the cache. If the cache is + * dirty, then it is sync'd to the on flash database. + */ + public void close() { + synchronized (this) { + this.sync(); + this.clear(); + db.close(); + db = null; + } + } + + /** + * Queries the cache with the given RfIdentification. + * + * If the emitter does not exist in the cache, it is + * added (from the database if known or a new "unknown" + * entry is created). + * + * @param id + * @return the emitter + * + */ + public RfEmitter get(RfIdentification id) { + if (id == null) + return null; + + synchronized (this) { + if (db == null) + return null; + String key = id.toString(); + RfEmitter rslt = workingSet.get(key); + if (rslt == null) { + rslt = db.getEmitter(id); + if (rslt == null) + rslt = new RfEmitter(id); + workingSet.put(key, rslt); + Log.i(TAG,"get('"+key+"') - Added to cache."); + } + rslt.resetAge(); + return rslt; + } + } + + /** + * Remove all entries from the cache. + */ + private void clear() { + synchronized (this) { + workingSet.clear(); + Log.i(TAG, "clear() - entry"); + } + } + + /** + * Updates the database entry for any new or changed emitters. + * Once the database has been synchronized, cull infrequently used + * entries. If our cache is still to big after culling, we reset + * our cache. + */ + public void sync() { + synchronized (this) { + if (db == null) + return; + boolean doSync = false; + + // Scan all of our emitters to see + // 1. If any have dirty data to sync to the flash database + // 2. If any have been unused long enough to remove from cache + + Set agedSet = new HashSet<>(); + for (Map.Entry e : workingSet.entrySet()) { + RfEmitter rfE = e.getValue(); + doSync |= rfE.syncNeeded(); + + Log.i(TAG,"sync('"+rfE.getRfIdent()+"') - Age: " + rfE.getAge()); + if (rfE.getAge() >= MAX_AGE) + agedSet.add(rfE.getRfIdent()); + rfE.incrementAge(); + } + + if (doSync) { + db.beginTransaction(); + for (Map.Entry e : workingSet.entrySet()) { + e.getValue().sync(db); + } + db.endTransaction(); + } + + // Remove aged out items from cache + for (RfIdentification id : agedSet) { + String key = id.toString(); + Log.i(TAG,"sync('"+key+"') - Aged out, removed from cache."); + workingSet.remove(key); + } + + if (workingSet.size() > MAX_WORKING_SET_SIZE) { + Log.i(TAG, "sync() - Clearing working set."); + workingSet.clear(); + } + } + } + + public HashSet getEmitters(RfEmitter.EmitterType rfType, BoundingBox bb) { + synchronized (this) { + if (db == null) + return null; + return db.getEmitters(rfType, bb); + } + } +} diff --git a/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/Kalman.java b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/Kalman.java new file mode 100644 index 0000000..2efae5b --- /dev/null +++ b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/Kalman.java @@ -0,0 +1,217 @@ +package org.microg.nlp.backend.dejavu; + +/* + * DejaVu - A location provider backend for microG/UnifiedNlp + * + */ + +/** + * Created by tfitch on 8/31/17. + */ + +/* + * This package inspired by https://github.com/villoren/KalmanLocationManager.git + */ + + +/** + * Copyright (c) 2014 Renato Villone + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Changes and modifications to the original file: + * + * Copyright (C) 2017 Tod Fitch + * + * 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. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import android.location.Location; +import android.os.Bundle; +import android.os.SystemClock; + +/** + * A two dimensional Kalman filter for estimating actual position from multiple + * measurements. We cheat and use two one dimensional Kalman filters which works + * because our two dimensions are orthogonal. + */ +class Kalman { + private static final double ALTITUDE_NOISE = 10.0; + + private static final float MOVING_THRESHOLD = 0.7f; // meters/sec (2.5 kph ~= 0.7 m/s) + private static final float MIN_ACCURACY = 3.0f; // Meters + + /** + * Three 1-dimension trackers, since the dimensions are independent and can avoid using matrices. + */ + private final Kalman1Dim mLatTracker; + private final Kalman1Dim mLonTracker; + private Kalman1Dim mAltTracker; + + /** + * Most recently computed mBearing. Only updated if we are moving. + */ + private float mBearing = 0.0f; + + /** + * Time of last update. Used to determine how stale our position is. + */ + private long mTimeOfUpdate; + + /** + * Number of samples filter has used. + */ + private long samples; + + /** + * + * @param location + */ + + public Kalman(Location location, double coordinateNoise) { + final double accuracy = location.getAccuracy(); + final double coordinateNoiseDegrees = coordinateNoise * BackendService.METER_TO_DEG; + double position, noise; + long timeMs = location.getTime(); + + // Latitude + position = location.getLatitude(); + noise = accuracy * BackendService.METER_TO_DEG; + mLatTracker = new Kalman1Dim(coordinateNoiseDegrees, timeMs); + mLatTracker.setState(position, 0.0, noise); + + // Longitude + position = location.getLongitude(); + noise = accuracy * Math.cos(Math.toRadians(location.getLatitude())) * BackendService.METER_TO_DEG; + mLonTracker = new Kalman1Dim(coordinateNoiseDegrees, timeMs); + mLonTracker.setState(position, 0.0, noise); + + // Altitude + if (location.hasAltitude()) { + position = location.getAltitude(); + noise = accuracy; + mAltTracker = new Kalman1Dim(ALTITUDE_NOISE, timeMs); + mAltTracker.setState(position, 0.0, noise); + } + mTimeOfUpdate = timeMs; + samples = 1; + } + + public synchronized void update(Location location) { + if (location == null) + return; + + // Reusable + final double accuracy = location.getAccuracy(); + double position, noise; + long timeMs = location.getTime(); + + predict(timeMs); + mTimeOfUpdate = timeMs; + samples++; + + // Latitude + position = location.getLatitude(); + noise = accuracy * BackendService.METER_TO_DEG; + mLatTracker.update(position, noise); + + // Longitude + position = location.getLongitude(); + noise = accuracy * Math.cos(Math.toRadians(location.getLatitude())) * BackendService.METER_TO_DEG ; + mLonTracker.update(position, noise); + + // Altitude + if (location.hasAltitude()) { + position = location.getAltitude(); + noise = accuracy; + if (mAltTracker == null) { + mAltTracker = new Kalman1Dim(ALTITUDE_NOISE, timeMs); + mAltTracker.setState(position, 0.0, noise); + } else { + mAltTracker.update(position, noise); + } + } + } + + private synchronized void predict(long timeMs) { + mLatTracker.predict(0.0, timeMs); + mLonTracker.predict(0.0, timeMs); + if (mAltTracker != null) + mAltTracker.predict(0.0, timeMs); + } + + // Allow others to override our sample count. They may want to have us report only the + // most recent samples. + public void setSamples(long s) { + samples = s; + } + + public long getSamples() { + return samples; + } + + public synchronized Location getLocation() { + Long timeMs = System.currentTimeMillis(); + final Location location = new Location(BackendService.LOCATION_PROVIDER); + + predict(timeMs); + location.setTime(timeMs); + location.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos()); + location.setLatitude(mLatTracker.getPosition()); + location.setLongitude(mLonTracker.getPosition()); + if (mAltTracker != null) + location.setAltitude(mAltTracker.getPosition()); + + float accuracy = (float) (mLatTracker.getAccuracy() * BackendService.DEG_TO_METER); + if (accuracy < MIN_ACCURACY) + accuracy = MIN_ACCURACY; + location.setAccuracy(accuracy); + + // Derive speed from degrees/ms in lat and lon + double latVeolocity = mLatTracker.getVelocity() * BackendService.DEG_TO_METER; + double lonVeolocity = mLonTracker.getVelocity() * BackendService.DEG_TO_METER * + Math.cos(Math.toRadians(location.getLatitude())); + float speed = (float) Math.sqrt((latVeolocity*latVeolocity)+(lonVeolocity*lonVeolocity)); + location.setSpeed(speed); + + // Compute bearing only if we are moving. Report old bearing + // if we are below our threshold for moving. + if (speed > MOVING_THRESHOLD) { + mBearing = (float) Math.toDegrees(Math.atan2(latVeolocity, lonVeolocity)); + } + location.setBearing(mBearing); + + Bundle extras = new Bundle(); + extras.putLong("AVERAGED_OF", samples); + location.setExtras(extras); + + return location; + } +} diff --git a/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/Kalman1Dim.java b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/Kalman1Dim.java new file mode 100644 index 0000000..f45aea4 --- /dev/null +++ b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/Kalman1Dim.java @@ -0,0 +1,236 @@ +package org.microg.nlp.backend.dejavu; +/* + * DejaVu - A location provider backend for microG/UnifiedNlp + */ + +/** + * Created by tfitch on 8/31/17. + */ + +/* + * This package inspired and largely copied from + * https://github.com/villoren/KalmanLocationManager.git + */ + +/** + * Copyright (c) 2014 Renato Villone + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Changes and modifications to this code: + * Copyright (C) 2017 Tod Fitch + * + * 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. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +class Kalman1Dim { + private final static double TIME_SECOND = 1000.0; // One second in milliseconds + + /** + * Minimal time step. + * + * Assume 200 KPH (55.6 m/s) and a maximum accuracy of 3 meters, then there is no need + * to update the filter any faster than 166.7 ms. + * + */ + private final static long TIME_STEP_MS = 150; + + /** + * Last prediction time + */ + private long mPredTime; + + /** + * Time step. Computed from differences in prediction times. + */ + private final double mt, mt2, mt2d2, mt3d2, mt4d4; + + /** + * Process noise covariance. Computed from time step and process noise + */ + private final double mQa, mQb, mQc, mQd; + + /** + * Estimated state + */ + private double mXa, mXb; + + /** + * Estimated covariance + */ + private double mPa, mPb, mPc, mPd; + + + /** + * Create a single dimension kalman filter. + * + * @param processNoise Standard deviation to calculate noise covariance from. + * @param timeMillisec The time the filter is started. + */ + public Kalman1Dim(double processNoise, long timeMillisec) { + double mProcessNoise = processNoise; + + mPredTime = timeMillisec; + + mt = ((double)TIME_STEP_MS) / TIME_SECOND; + mt2 = mt * mt; + mt2d2 = mt2 / 2.0; + mt3d2 = mt2 * mt / 2.0; + mt4d4 = mt2 * mt2 / 4.0; + + // Process noise covariance + double n2 = mProcessNoise * mProcessNoise; + mQa = n2 * mt4d4; + mQb = n2 * mt3d2; + mQc = mQb; + mQd = n2 * mt2; + + // Estimated covariance + mPa = mQa; + mPb = mQb; + mPc = mQc; + mPd = mQd; + } + + /** + * Reset the filter to the given state. + *

+ * Should be called after creation, unless position and velocity are assumed to be both zero. + * + * @param position + * @param velocity + * @param noise + */ + public void setState(double position, double velocity, double noise) { + + // State vector + mXa = position; + mXb = velocity; + + // Covariance + double n2 = noise * noise; + mPa = n2 * mt4d4; + mPb = n2 * mt3d2; + mPc = mPb; + mPd = n2 * mt2; + } + + /** + * Predict state. + * + * @param acceleration Should be 0 unless there's some sort of control input (a gas pedal, for instance). + * @param timeMillisec The time the prediction is for. + */ + public void predict(double acceleration, long timeMillisec) { + + long delta_t = timeMillisec - mPredTime; + while (delta_t > TIME_STEP_MS) { + mPredTime = mPredTime + TIME_STEP_MS; + + // x = F.x + G.u + mXa = mXa + mXb * mt + acceleration * mt2d2; + mXb = mXb + acceleration * mt; + + // P = F.P.F' + Q + double Pdt = mPd * mt; + double FPFtb = mPb + Pdt; + double FPFta = mPa + mt * (mPc + FPFtb); + double FPFtc = mPc + Pdt; + double FPFtd = mPd; + + mPa = FPFta + mQa; + mPb = FPFtb + mQb; + mPc = FPFtc + mQc; + mPd = FPFtd + mQd; + + delta_t = timeMillisec - mPredTime; + } + } + + /** + * Update (correct) with the given measurement. + * + * @param position + * @param noise + */ + public void update(double position, double noise) { + + double r = noise * noise; + + // y = z - H . x + double y = position - mXa; + + // S = H.P.H' + R + double s = mPa + r; + double si = 1.0 / s; + + // K = P.H'.S^(-1) + double Ka = mPa * si; + double Kb = mPc * si; + + // x = x + K.y + mXa = mXa + Ka * y; + mXb = mXb + Kb * y; + + // P = P - K.(H.P) + double Pa = mPa - Ka * mPa; + double Pb = mPb - Ka * mPb; + double Pc = mPc - Kb * mPa; + double Pd = mPd - Kb * mPb; + + mPa = Pa; + mPb = Pb; + mPc = Pc; + mPd = Pd; + + } + + /** + * @return Estimated position. + */ + public double getPosition() { + return mXa; + } + + /** + * @return Estimated velocity. + */ + public double getVelocity() { + return mXb; + } + + /** + * @return Accuracy + */ + public double getAccuracy() { + return Math.sqrt(mPd / mt2); + } +} diff --git a/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/LogToFile.java b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/LogToFile.java new file mode 100644 index 0000000..b64e914 --- /dev/null +++ b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/LogToFile.java @@ -0,0 +1,130 @@ +package org.microg.nlp.backend.dejavu; + +import android.util.Log; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import android.location.Location; +import org.microg.nlp.api.LocationBackend; + +public class LogToFile { + + private static final String TAG = LogToFile.class.getName(); + + private static final String TIME_DATE_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS"; + + private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat(TIME_DATE_PATTERN); + + public static String logFilePathname; + public static boolean logToFileEnabled; + public static int logFileHoursOfLasting; + private static Calendar logFileAtTheEndOfLive; + + public static void appendLog(String tag, String text) { + appendLog(tag, text, null); + } + + public static void appendLogWithLocationBackend(String tag, String text, LocationBackend locationBackend) { + if (locationBackend == null) { + appendLog(tag, text + "null", null); + } else { + appendLog(tag, text + locationBackend.toString(), null); + } + } + + public static void appendLogWithLocation(String tag, String text, Location location) { + if (location == null) { + appendLog(tag, text + "null", null); + } else { + appendLog(tag, text + location.toString(), null); + } + } + + public static void appendLog(String tag, String text, Throwable throwable) { + + logToFileEnabled = false; + logFilePathname = "/sdcard/Download/logs/log-dejavu.txt"; + logFileHoursOfLasting = 96; + + if (!logToFileEnabled || (logFilePathname == null)) { + //Log.e(TAG, "logging not allowed " + logToFileEnabled + " " + logFilePathname); + return; + } + + File logFile = new File(logFilePathname); + + Date now = new Date(); + try { + if (logFile.exists()) { + if (logFileAtTheEndOfLive == null) { + boolean succeeded = initFileLogging(logFile); + if (!succeeded) { + createNewLogFile(logFile, now); + } + } else if(Calendar.getInstance().after(logFileAtTheEndOfLive)) { + logFile.delete(); + createNewLogFile(logFile, now); + } + } else { + createNewLogFile(logFile, now); + } + BufferedWriter buf = new BufferedWriter(new FileWriter(logFile, true)); + buf.append(DATE_FORMATTER.format(now)); + buf.append(" "); + buf.append(tag); + buf.append(" - "); + buf.append(text); + if (throwable != null) { + buf.append(" - "); + buf.append(throwable.getMessage()); + for (StackTraceElement ste: throwable.getStackTrace()) { + buf.newLine(); + buf.append(ste.toString()); + } + } + buf.newLine(); + buf.close(); + } catch (IOException e) { + Log.e(TAG, e.getMessage()); + } + } + + private static boolean initFileLogging(File logFile) { + char[] logFileDateCreatedBytes = new char[TIME_DATE_PATTERN.length()]; + Date logFileDateCreated; + FileReader logFileReader = null; + try { + logFileReader = new FileReader(logFile); + logFileReader.read(logFileDateCreatedBytes); + logFileDateCreated = DATE_FORMATTER.parse(new String(logFileDateCreatedBytes)); + } catch (Exception e) { + return false; + } finally { + if (logFileReader != null) { + try { + logFileReader.close(); + } catch (IOException ex) { + + } + } + } + initEndOfLive(logFileDateCreated); + return true; + } + + private static void initEndOfLive(Date logFileDateCreated) { + logFileAtTheEndOfLive = Calendar.getInstance(); + logFileAtTheEndOfLive.setTime(logFileDateCreated); + logFileAtTheEndOfLive.add(Calendar.HOUR_OF_DAY, logFileHoursOfLasting); + } + + private static void createNewLogFile(File logFile, Date dateOfCreation) throws IOException { + logFile.createNewFile(); + initEndOfLive(dateOfCreation); + } +} diff --git a/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/WeightedAverage.java b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/WeightedAverage.java new file mode 100644 index 0000000..1a66625 --- /dev/null +++ b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/WeightedAverage.java @@ -0,0 +1,142 @@ +package org.microg.nlp.backend.dejavu; + +/** + * Created by tfitch on 10/30/17. + */ + +import android.location.Location; +import android.os.Bundle; + +import org.microg.nlp.backend.dejavu.database.RfEmitter; +//import android.util.Log; + +class WeightedAverage { + private static final String TAG="DejaVu wgtAvg"; + private static final float MINIMUM_BELIEVABLE_ACCURACY = 15.0F; + + private int count; + private long timeMs; + private long mElapsedRealtimeNanos; + + private class simpleWeightedAverage { + // See: https://physics.stackexchange.com/a/329412 for details about estimating + // error on weighted averages + private double wSum; + private double wSum2; + private double mean; + private double sdAccum; + + simpleWeightedAverage() { + reset(); + } + + void reset() { + wSum = wSum2 = mean = sdAccum = 0.0; + } + + void add(double x, double sd, double weight) { + wSum = wSum + weight; + wSum2 = wSum2 + (weight * weight); + + double oldMean = mean; + mean = oldMean + (weight / wSum) * (x - oldMean); + + sdAccum += (weight*weight)*(sd*sd); + } + + double getMean() { + return mean; + } + + double getStdDev() { + return Math.sqrt((1.0/wSum2)*sdAccum); + } + } + + private final simpleWeightedAverage latEst; + private final simpleWeightedAverage lonEst; + + WeightedAverage() { + latEst = new simpleWeightedAverage(); + lonEst = new simpleWeightedAverage(); + reset(); + } + + private void reset() { + latEst.reset(); + lonEst.reset(); + + count = 0; + timeMs = 0; + mElapsedRealtimeNanos = 0; + } + + public void add(Location loc) { + if (loc == null) + return; + + // + // We weight each location based on the signal strength, the higher the + // strength the higher the weight. And we also use the estimated + // coverage diameter. The larger the diameter, the lower the weight. + // + // ASU (signal strength) has been hard limited to always be >= 1 + // Accuracy (estimate of coverage radius) has been hard limited to always + // be >= a emitter type minimum. + // + // So we are safe in computing the weight by dividing ASU by Accuracy. + // + + float asu = loc.getExtras().getInt(RfEmitter.LOC_ASU); + double weight = asu/ loc.getAccuracy(); + + count++; + //Log.d(TAG,"add() entry: weight="+weight+", count="+count); + + // + // Our input has an accuracy based on the detection of the edge of the coverage area. + // So assume that is a high (two sigma) probability and, worse, assume we can turn that + // into normal distribution error statistic. We will assume our standard deviation (one + // sigma) is half of our accuracy. + // + double stdDev = loc.getAccuracy()*BackendService.METER_TO_DEG/2.0; + double cosLat = Math.max(BackendService.MIN_COS, Math.cos(Math.toRadians(loc.getLatitude()))); + + latEst.add(loc.getLatitude(),stdDev,weight); + lonEst.add(loc.getLongitude(),stdDev*cosLat, weight); + + timeMs = Math.max(timeMs,loc.getTime()); + mElapsedRealtimeNanos = Math.max(mElapsedRealtimeNanos,loc.getElapsedRealtimeNanos()); + } + + public Location result() { + if (count < 1) + return null; + + final Location location = new Location(BackendService.LOCATION_PROVIDER); + + location.setTime(timeMs); + location.setElapsedRealtimeNanos(mElapsedRealtimeNanos); + + location.setLatitude(latEst.getMean()); + location.setLongitude(lonEst.getMean()); + + // + // Accuracy estimate is in degrees, convert to meters for output. + // We calculate North-South and East-West independently, convert to a + // circular radius by finding the length of the diagonal. + // + double sdMetersLat = latEst.getStdDev() * BackendService.DEG_TO_METER; + double cosLat = Math.max(BackendService.MIN_COS, Math.cos(Math.toRadians(latEst.getMean()))); + double sdMetersLon = lonEst.getStdDev() * BackendService.DEG_TO_METER * cosLat; + + float acc = (float) Math.max(Math.sqrt((sdMetersLat*sdMetersLat)+(sdMetersLon*sdMetersLon)),MINIMUM_BELIEVABLE_ACCURACY); + location.setAccuracy(acc); + + Bundle extras = new Bundle(); + extras.putLong("AVERAGED_OF", count); + location.setExtras(extras); + + return location; + } +} diff --git a/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/database/BoundingBox.java b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/database/BoundingBox.java new file mode 100644 index 0000000..d213f6d --- /dev/null +++ b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/database/BoundingBox.java @@ -0,0 +1,187 @@ +package org.microg.nlp.backend.dejavu.database; +/* + * DejaVu - A location provider backend for microG/UnifiedNlp + * + * Copyright (C) 2017 Tod Fitch + * + * 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. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * Created by tfitch on 9/28/17. + */ + +import android.location.Location; + +import org.microg.nlp.backend.dejavu.BackendService; + +public class BoundingBox { + private double north; + private double south; + private double east; + private double west; + private double center_lat; + private double center_lon; + private double radius; + private double radius_ns; + private double radius_ew; + + public BoundingBox() { + reset(); + } + + public BoundingBox(Location loc) { + reset(); + update(loc); + } + + public BoundingBox(double lat, double lon, float radius) { + reset(); + update(lat, lon, radius); + } + + public BoundingBox(Database.EmitterInfo info) { + reset(); + update(info.latitude, info.longitude, info.radius_ns, info.radius_ew); + } + + /** + * Expand, if needed, the bounding box to include the coverage area + * implied by a location. + * @param loc A record describing the coverage of an RF emitter. + */ + private boolean update(Location loc) { + return update(loc.getLatitude(), loc.getLongitude(), loc.getAccuracy()); + } + + /** + * Expand bounding box to include an emitter at a lat/lon with a + * specified radius. + * + * @param lat The center latitude for the coverage area. + * @param lon The center longitude for the coverage area. + * @param radius The radius of the coverage area. + */ + private boolean update(double lat, double lon, float radius) { + return update(lat, lon, radius, radius); + } + + /** + * Expand bounding box to include an emitter at a lat/lon with a + * specified radius. + * + * @param lat The center latitude for the coverage area. + * @param lon The center longitude for the coverage area. + * @param radius_ns The distance from the center to the north (or south) edge. + * @param radius_ew The distance from the center to the east (or west) edge. + */ + private boolean update(double lat, double lon, float radius_ns, float radius_ew) { + double locNorth = lat + (radius_ns * BackendService.METER_TO_DEG); + double locSouth = lat - (radius_ns * BackendService.METER_TO_DEG); + double cosLat = Math.cos(Math.toRadians(lat)); + double locEast = lon + (radius_ew * BackendService.METER_TO_DEG) * cosLat; + double locWest = lon - (radius_ew * BackendService.METER_TO_DEG) * cosLat; + + // Can't just "update(locNorth, locWest) || update(locSouth, locEast)" + // because we need the second update to be called even if the first + // returns true. + boolean rslt = update(locNorth, locWest); + if (update(locSouth, locEast)) + rslt = true; + return rslt; + } + + /** + * Update the bounding box to include a point at the specified lat/lon + * @param lat The latitude to be included in the bounding box + * @param lon The longitude to be included in the bounding box + */ + public boolean update(double lat, double lon) { + boolean rslt = false; + + if (lat > north) { + north = lat; + rslt = true; + } + if (lat < south) { + south = lat; + rslt = true; + } + if (lon > east) { + east = lon; + rslt = true; + } + if (lon < west) { + west = lon; + rslt = true; + } + + if (rslt) { + center_lat = (north + south)/2.0; + center_lon = (east + west)/2.0; + + radius_ns = (float)((north - center_lat) * BackendService.DEG_TO_METER); + double cosLat = Math.max(Math.cos(Math.toRadians(center_lat)),BackendService.MIN_COS); + radius_ew = (float)(((east - center_lon) * BackendService.DEG_TO_METER) / cosLat); + + radius = Math.sqrt(radius_ns*radius_ns + radius_ew*radius_ew); + } + + return rslt; + } + + public double getNorth() { + return north; + } + + public double getSouth() { + return south; + } + + public double getEast() { + return east; + } + + public double getWest() { + return west; + } + + public double getCenter_lat() { return center_lat; } + + public double getCenter_lon() { return center_lon; } + + public double getRadius() { return radius; } + + public double getRadius_ns() { return radius_ns; } + + public double getRadius_ew() { return radius_ew; } + + @Override + public String toString() { + return "(" + north + "," + west + "," + south + "," + east + "," + center_lat + "," + center_lon + "," + radius_ns+ "," + radius_ew+ "," + radius + ")"; + } + + private void reset() { + north = -91.0; // Impossibly south + south = 91.0; // Impossibly north + east = -181.0; // Impossibly west + west = 181.0; // Impossibly east + center_lat = 0.0; // Center at "null island" + center_lon = 0.0; + radius = 0.0; // No coverage radius + radius_ns = 0.0; + radius_ew = 0.0; + } + +} diff --git a/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/database/Database.java b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/database/Database.java new file mode 100644 index 0000000..92aedd1 --- /dev/null +++ b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/database/Database.java @@ -0,0 +1,444 @@ +package org.microg.nlp.backend.dejavu.database; +/* + * DejaVu - A location provider backend for microG/UnifiedNlp + * + * Copyright (C) 2017 Tod Fitch + * + * 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. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +/** + * Created by tfitch on 9/1/17. + */ + +import android.content.Context; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteStatement; +import android.util.Log; + +import java.util.HashSet; + +/** + * Interface to our on flash SQL database. Note that these methods are not + * thread safe. However all access to the database is through the Cache object + * which is thread safe. + */ +public class Database extends SQLiteOpenHelper { + private static final String TAG = "DejaVu DB"; + + private static final int VERSION = 3; + private static final String NAME = "rf.db"; + + private static final String TABLE_SAMPLES = "emitters"; + + private static final String COL_HASH = "rfHash"; // v3 of database + private static final String COL_TYPE = "rfType"; + private static final String COL_RFID = "rfID"; + private static final String COL_TRUST = "trust"; + private static final String COL_LAT = "latitude"; + private static final String COL_LON = "longitude"; + private static final String COL_RAD = "radius"; // v1 of database + private static final String COL_RAD_NS = "radius_ns"; // v2 of database + private static final String COL_RAD_EW = "radius_ew"; // v2 of database + private static final String COL_NOTE = "note"; + + private SQLiteDatabase database; + private boolean withinTransaction; + private boolean updatesMade; + + private SQLiteStatement sqlSampleInsert; + private SQLiteStatement sqlSampleUpdate; + private SQLiteStatement sqlAPdrop; + + public class EmitterInfo { + public double latitude; + public double longitude; + public float radius_ns; + public float radius_ew; + public long trust; + public String note; + } + + public Database(Context context) { + super(context, NAME, null, VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + database = db; + withinTransaction = false; + // Always create version 1 of database, then update the schema + // in the same order it might occur "in the wild". Avoids having + // to check to see if the table exists (may be old version) + // or not (can be new version). + db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_SAMPLES + "(" + + COL_RFID + " STRING PRIMARY KEY, " + + COL_TYPE + " STRING, " + + COL_TRUST + " INTEGER, " + + COL_LAT + " REAL, " + + COL_LON + " REAL, " + + COL_RAD + " REAL, " + + COL_NOTE + " STRING);"); + + onUpgrade(db, 1, VERSION); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion < 2) + upGradeToVersion2(db); + if (oldVersion < 3) + upGradeToVersion3(db); + } + + private void upGradeToVersion2(SQLiteDatabase db) { + Log.i(TAG, "upGradeToVersion2(): Entry"); + // Sqlite3 does not support dropping columns so we create a new table with our + // current fields and copy the old data into it. + db.execSQL("BEGIN TRANSACTION;"); + db.execSQL("ALTER TABLE " + TABLE_SAMPLES + " RENAME TO " + TABLE_SAMPLES + "_old;"); + db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_SAMPLES + "(" + + COL_RFID + " STRING PRIMARY KEY, " + + COL_TYPE + " STRING, " + + COL_TRUST + " INTEGER, " + + COL_LAT + " REAL, " + + COL_LON + " REAL, " + + COL_RAD_NS + " REAL, " + + COL_RAD_EW + " REAL, " + + COL_NOTE + " STRING);"); + + db.execSQL("INSERT INTO " + TABLE_SAMPLES + "(" + + COL_RFID + ", " + + COL_TYPE + ", " + + COL_TRUST + ", " + + COL_LAT + ", " + + COL_LON + ", " + + COL_RAD_NS + ", " + + COL_RAD_EW + ", " + + COL_NOTE + + ") SELECT " + + COL_RFID + ", " + + COL_TYPE + ", " + + COL_TRUST + ", " + + COL_LAT + ", " + + COL_LON + ", " + + COL_RAD + ", " + + COL_RAD + ", " + + COL_NOTE + + " FROM " + TABLE_SAMPLES + "_old;"); + db.execSQL("DROP TABLE " + TABLE_SAMPLES + "_old;"); + db.execSQL("COMMIT;"); + } + + public void clearDatabase() { + SQLiteDatabase db = getWritableDatabase(); + db.delete(TABLE_SAMPLES, null, null); + db.close(); + } + + public long getRowsCount() { + SQLiteDatabase db = getReadableDatabase(); + long count = DatabaseUtils.queryNumEntries(db, TABLE_SAMPLES); + db.close(); + return count; + } + + private void upGradeToVersion3(SQLiteDatabase db) { + Log.i(TAG, "upGradeToVersion3(): Entry"); + + // We are changing our key field to a new text field that contains a hash of + // of the ID and type. In addition, we are dealing with a Lint complaint about + // using a string field where we ought to be using a text field. + + db.execSQL("BEGIN TRANSACTION;"); + db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_SAMPLES + "_new (" + + COL_HASH + " TEXT PRIMARY KEY, " + + COL_RFID + " TEXT, " + + COL_TYPE + " TEXT, " + + COL_TRUST + " INTEGER, " + + COL_LAT + " REAL, " + + COL_LON + " REAL, " + + COL_RAD_NS + " REAL, " + + COL_RAD_EW + " REAL, " + + COL_NOTE + " TEXT);"); + + SQLiteStatement insert = db.compileStatement("INSERT INTO " + + TABLE_SAMPLES + "_new("+ + COL_HASH + ", " + + COL_RFID + ", " + + COL_TYPE + ", " + + COL_TRUST + ", " + + COL_LAT + ", " + + COL_LON + ", " + + COL_RAD_NS + ", " + + COL_RAD_EW + ", " + + COL_NOTE + ") " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);"); + + String query = "SELECT " + + COL_RFID+","+COL_TYPE+","+COL_TRUST+","+COL_LAT+","+COL_LON+","+COL_RAD_NS+","+COL_RAD_EW+","+COL_NOTE+" "+ + "FROM " + TABLE_SAMPLES + ";"; + + Cursor cursor = db.rawQuery(query, null); + try { + if (cursor.moveToFirst()) { + do { + String rfId = cursor.getString(0); + String rftype = cursor.getString(1); + if (rftype.equals("WLAN")) + rftype = RfEmitter.EmitterType.WLAN_24GHZ.toString(); + RfIdentification rfid = new RfIdentification(rfId, RfEmitter.typeOf(rftype)); + String hash = rfid.getUniqueId(); + + // Log.i(TAG,"upGradeToVersion2(): Updating '"+rfId.toString()+"'"); + + insert.bindString(1, hash); + insert.bindString(2, rfId); + insert.bindString(3, rftype); + insert.bindString(4, cursor.getString(2)); + insert.bindString(5, cursor.getString(3)); + insert.bindString(6, cursor.getString(4)); + insert.bindString(7, cursor.getString(5)); + insert.bindString(8, cursor.getString(6)); + insert.bindString(9, cursor.getString(7)); + + insert.executeInsert(); + insert.clearBindings(); + } while (cursor.moveToNext()); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + db.execSQL("DROP TABLE " + TABLE_SAMPLES + ";"); + db.execSQL("ALTER TABLE " + TABLE_SAMPLES + "_new RENAME TO " + TABLE_SAMPLES + ";"); + db.execSQL("COMMIT;"); + } + + @Override + public void onOpen(SQLiteDatabase db) { + super.onOpen(db); + } + + /** + * Start an update operation. + * + * We make sure we are not already in a transaction, make sure + * our database is writeable, compile the insert, update and drop + * statements that are likely to be used, etc. Then we actually + * start the transaction on the underlying SQL database. + */ + public void beginTransaction() { + //Log.i(TAG,"beginTransaction()"); + if (withinTransaction) { + Log.i(TAG,"beginTransaction() - Already in a transaction?"); + return; + } + withinTransaction = true; + updatesMade = false; + database = getWritableDatabase(); + + sqlSampleInsert = database.compileStatement("INSERT INTO " + + TABLE_SAMPLES + "("+ + COL_HASH + ", " + + COL_RFID + ", " + + COL_TYPE + ", " + + COL_TRUST + ", " + + COL_LAT + ", " + + COL_LON + ", " + + COL_RAD_NS + ", " + + COL_RAD_EW + ", " + + COL_NOTE + ") " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);"); + + sqlSampleUpdate = database.compileStatement("UPDATE " + + TABLE_SAMPLES + " SET "+ + COL_TRUST + "=?, " + + COL_LAT + "=?, " + + COL_LON + "=?, " + + COL_RAD_NS + "=?, " + + COL_RAD_EW + "=?, " + + COL_NOTE + "=? " + + "WHERE " + COL_HASH + "=?;"); + + sqlAPdrop = database.compileStatement("DELETE FROM " + + TABLE_SAMPLES + + " WHERE " + COL_HASH + "=?;"); + + database.beginTransaction(); + } + + /** + * End a transaction. If we actually made any changes then we mark + * the transaction as successful. Once marked as successful we + * end the transaction with the underlying SQL database. + */ + public void endTransaction() { + //Log.i(TAG,"endTransaction()"); + if (!withinTransaction) { + Log.i(TAG,"Asked to end transaction but we are not in one???"); + } + + if (updatesMade) { + //Log.i(TAG,"endTransaction() - Setting transaction successful."); + database.setTransactionSuccessful(); + } + updatesMade = false; + database.endTransaction(); + withinTransaction = false; + } + + /** + * Drop an RF emitter from the database. + * + * @param emitter The emitter to be dropped. + */ + public void drop(RfEmitter emitter) { + //Log.i(TAG, "Dropping " + emitter.logString() + " from db"); + + sqlAPdrop.bindString(1, emitter.getUniqueId()); + sqlAPdrop.executeInsert(); + sqlAPdrop.clearBindings(); + updatesMade = true; + } + + /** + * Insert a new RF emitter into the database. + * + * @param emitter The emitter to be added. + */ + public void insert(RfEmitter emitter) { + Log.i(TAG, "Inserting " + emitter.logString() + " into db"); + sqlSampleInsert.bindString(1, emitter.getUniqueId()); + sqlSampleInsert.bindString(2, emitter.getId()); + sqlSampleInsert.bindString(3, String.valueOf(emitter.getType())); + sqlSampleInsert.bindString(4, String.valueOf(emitter.getTrust())); + sqlSampleInsert.bindString(5, String.valueOf(emitter.getLat())); + sqlSampleInsert.bindString(6, String.valueOf(emitter.getLon())); + sqlSampleInsert.bindString(7, String.valueOf(emitter.getRadiusNS())); + sqlSampleInsert.bindString(8, String.valueOf(emitter.getRadiusEW())); + sqlSampleInsert.bindString(9, emitter.getNote()); + + sqlSampleInsert.executeInsert(); + sqlSampleInsert.clearBindings(); + updatesMade = true; + } + + /** + * Update information about an emitter already existing in the database + * + * @param emitter The emitter to be updated + */ + public void update(RfEmitter emitter) { + //Log.i(TAG, "Updating " + emitter.logString() + " in db"); + + // the data fields + sqlSampleUpdate.bindString(1, String.valueOf(emitter.getTrust())); + sqlSampleUpdate.bindString(2, String.valueOf(emitter.getLat())); + sqlSampleUpdate.bindString(3, String.valueOf(emitter.getLon())); + sqlSampleUpdate.bindString(4, String.valueOf(emitter.getRadiusNS())); + sqlSampleUpdate.bindString(5, String.valueOf(emitter.getRadiusEW())); + sqlSampleUpdate.bindString(6, emitter.getNote()); + + // the Where fields + sqlSampleUpdate.bindString(7, emitter.getUniqueId()); + sqlSampleUpdate.executeInsert(); + sqlSampleUpdate.clearBindings(); + updatesMade = true; + } + + /** + * Return a list of all emitters of a specified type within a bounding box. + * + * @param rfType The type of emitter the caller is interested in + * @param bb The lat,lon bounding box. + * @return A collection of RF emitter identifications + */ + public HashSet getEmitters(RfEmitter.EmitterType rfType, BoundingBox bb) { + HashSet rslt = new HashSet<>(); + String query = "SELECT " + + COL_RFID + " " + + " FROM " + TABLE_SAMPLES + + " WHERE " + COL_TYPE + "='" + rfType + + "' AND " + COL_LAT + ">='" + bb.getSouth() + + "' AND " + COL_LAT + "<='" + bb.getNorth() + + "' AND " + COL_LON + ">='" + bb.getWest() + + "' AND " + COL_LON + "<='" + bb.getEast() + "';"; + + //Log.i(TAG, "getEmitters(): query='"+query+"'"); + Cursor cursor = getReadableDatabase().rawQuery(query, null); + try { + if (cursor.moveToFirst()) { + do { + RfIdentification e = new RfIdentification(cursor.getString(0), rfType); + rslt.add(e); + } while (cursor.moveToNext()); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return rslt; + } + + /** + * Get all the information we have on an RF emitter + * + * @param ident The identification of the emitter caller wants + * @return A emitter object with all the information we have. Or null if we have nothing. + */ + public RfEmitter getEmitter(RfIdentification ident) { + RfEmitter rslt = null; + + String query = "SELECT " + + COL_TYPE + ", " + + COL_TRUST + ", " + + COL_LAT + ", " + + COL_LON + ", " + + COL_RAD_NS+ ", " + + COL_RAD_EW+ ", " + + COL_NOTE + " " + + " FROM " + TABLE_SAMPLES + + " WHERE " + COL_HASH + "='" + ident.getUniqueId() + "';"; + + // Log.i(TAG, "getEmitter(): query='"+query+"'"); + Cursor cursor = getReadableDatabase().rawQuery(query, null); + try { + if (cursor.moveToFirst()) { + rslt = new RfEmitter(ident); + EmitterInfo ei = new EmitterInfo(); + ei.trust = (int) cursor.getLong(1); + ei.latitude = cursor.getDouble(2); + ei.longitude = cursor.getDouble(3); + ei.radius_ns = (float) cursor.getDouble(4); + ei.radius_ew = (float) cursor.getDouble(5); + ei.note = cursor.getString(6); + if (ei.note == null) + ei.note = ""; + rslt.updateInfo(ei); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return rslt; + } +} diff --git a/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/database/Observation.java b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/database/Observation.java new file mode 100644 index 0000000..533b64e --- /dev/null +++ b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/database/Observation.java @@ -0,0 +1,118 @@ +package org.microg.nlp.backend.dejavu.database; +/* + * DejaVu - A location provider backend for microG/UnifiedNlp + * + * Copyright (C) 2017 Tod Fitch + * + * 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. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * Created by tfitch on 10/5/17. + */ + +import android.os.SystemClock; + +import org.microg.nlp.backend.dejavu.BackendService; + +/** + * A single observation made of a RF emitter. + * + * Used to convey all the information we have collected in the foreground about + * a RF emitter we have seen to the background thread that actually does the + * heavy lifting. + * + * It contains an identifier for the RF emitter (type and id), the received signal + * level and optionally a note about about the emitter. + */ + +public class Observation implements Comparable { + private final RfIdentification ident; + private int asu; + private String note; + + private long mLastUpdateTimeMs; + private long mElapsedRealtimeNanos; + + public Observation(String id, RfEmitter.EmitterType t) { + ident = new RfIdentification(id, t); + note = ""; + asu = BackendService.MINIMUM_ASU; + mLastUpdateTimeMs = System.currentTimeMillis(); + mElapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos(); + } + + public int compareTo(Observation o) { + int rslt = o.asu - asu; + if (rslt == 0) + rslt = ident.compareTo(o.ident); + return rslt; + } + + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + return (toString().compareTo(o.toString()) == 0); + } + + @Override + public int hashCode() { + int result = 1; + + if (ident != null) + result = ident.hashCode(); + result = (result << 31) + asu; + return result; + } + + public RfIdentification getIdent() { + return ident; + } + + public void setAsu(int signal) { + if (signal > BackendService.MAXIMUM_ASU) + asu = BackendService.MAXIMUM_ASU; + else if (signal < BackendService.MINIMUM_ASU) + asu = BackendService.MINIMUM_ASU; + else + asu = signal; + } + + public int getAsu() { + return asu; + } + + public long getLastUpdateTimeMs() { + return mLastUpdateTimeMs; + } + + public long getElapsedRealtimeNanos() { + return mElapsedRealtimeNanos; + } + + public void setNote(String n) { + note = n; + } + + public String getNote() { + return note; + } + + public String toString() { + return ident.toString() + ", asu=" + asu + ", note='" + note + "'"; + } +} diff --git a/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/database/RfEmitter.java b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/database/RfEmitter.java new file mode 100644 index 0000000..daa5bdb --- /dev/null +++ b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/database/RfEmitter.java @@ -0,0 +1,787 @@ +package org.microg.nlp.backend.dejavu.database; + +/* + * DejaVu - A location provider backend for microG/UnifiedNlp + * + * Copyright (C) 2017 Tod Fitch + * + * 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. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * Created by tfitch on 8/27/17. + */ + +import android.location.Location; +import android.os.Bundle; +import android.util.Log; + +import org.microg.nlp.backend.dejavu.BackendService; + +import java.util.Locale; + +/** + * Models everything we know about an RF emitter: Its identification, most recently received + * signal level, an estimate of its coverage (center point and radius), how much we trust + * the emitter (can we use information about it to compute a position), etc. + * + * Starting with v2 of the database, we store a north-south radius and an east-west radius which + * allows for a rectangular bounding box rather than a square one. + * + * When an RF emitter is first observed we create a new object and, if information exists in + * the database, populate it from saved information. + * + * Periodically we sync our current information about the emitter back to the flash memory + * based storage. + * + * Trust is incremented everytime we see the emitter and the new observation has data compatible + * with our current model. We decrease (or set to zero) our trust if it we think we should have + * seen the emitter at our current location or if it looks like the emitter may have moved. + */ +public class RfEmitter { + private final static String TAG = "DejaVu RfEmitter"; + + private static final long SECONDS = 1000; // In milliseconds + private static final long MINUTES = 60 * SECONDS; + private static final long HOURS = 60 * MINUTES; + private static final long DAYS = HOURS * 24; + + private static final long METERS = 1; + private static final long KM = METERS * 1000; + + private static final long MINIMUM_TRUST = 0; + private static final long REQUIRED_TRUST = 48; + private static final long MAXIMUM_TRUST = 100; + + // Tag/names for additional information on location records + public static final String LOC_RF_ID = "rfid"; + public static final String LOC_RF_TYPE = "rftype"; + public static final String LOC_ASU = "asu"; + public static final String LOC_MIN_COUNT = "minCount"; + + public enum EmitterType {WLAN_24GHZ, WLAN_5GHZ, MOBILE, INVALID} + + public enum EmitterStatus { + STATUS_UNKNOWN, // Newly discovered emitter, no data for it at all + STATUS_NEW, // Not in database but we've got location data for it + STATUS_CHANGED, // In database but something has changed + STATUS_CACHED, // In database no changes pending + STATUS_BLACKLISTED // Has been blacklisted + } + + public static class RfCharacteristics { + public final float reqdGpsAccuracy; // GPS accuracy needed in meters + public final float minimumRange; // Minimum believable coverage radius in meters + public final float typicalRange; // Typical range expected + public final float moveDetectDistance; // Maximum believable coverage radius in meters + public final long discoveryTrust; // Assumed trustiness of a rust an emitter seen for the first time. + public final long incrTrust; // Amount to increase trust + public final long decrTrust; // Amount to decrease trust + public final long minCount; // Minimum number of emitters before we can estimate location + + RfCharacteristics( float gps, + float min, + float typical, + float moveDist, + long newTrust, + long incr, + long decr, + long minC) { + reqdGpsAccuracy = gps; + minimumRange = min; + typicalRange = typical; + moveDetectDistance = moveDist; + discoveryTrust = newTrust; + incrTrust = incr; + decrTrust = decr; + minCount = minC; + } + } + + private RfCharacteristics ourCharacteristics; + + private EmitterType type; + private String id; + private long trust; + private BoundingBox coverage; + private String note; + + private Observation mLastObservation; + + private int ageSinceLastUse; // Count of periods since last used (for caching purposes) + + private EmitterStatus status; + + public RfEmitter(RfIdentification ident) { + initSelf(ident.getRfType(), ident.getRfId()); + } + + public RfEmitter(Observation o) { + initSelf(o.getIdent().getRfType(), o.getIdent().getRfId()); + mLastObservation = o; + } + + public RfEmitter(EmitterType mType, String ident) { + initSelf(mType, ident); + } + + /** + * Shared/uniform initialization, called from the various constructors we allow. + * + * @param mType The type of the RF emitter (WLAN_24GHZ, MOBILE, etc.) + * @param ident The identification of the emitter. Must be unique within type + */ + private void initSelf(EmitterType mType, String ident) { + type = mType; + id = ident; + coverage = null; + mLastObservation = null; + ourCharacteristics = getRfCharacteristics(mType); + trust = ourCharacteristics.discoveryTrust; + note = ""; + resetAge(); + status = EmitterStatus.STATUS_UNKNOWN; + } + + /** + * On equality check, we only check that our type and ID match as that + * uniquely identifies our RF emitter. + * + * @param o The object to check for equality + * @return True if the objects should be considered the same. + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof RfIdentification)) return false; + + RfIdentification e = (RfIdentification) o; + return getRfIdent().equals(e); + } + + /** + * Hash code is used to determine unique objects. Our "uniqueness" is + * based on which "real life" RF emitter we model, not our current + * coverage, etc. So our hash code should be the same as the hash + * code of our identification. + * + * @return A hash code for this object. + */ + @Override + public int hashCode() { + return getRfIdent().hashCode(); + } + + public String getUniqueId() { + return getRfIdent().getUniqueId(); + } + + public EmitterType getType() { + return type; + } + + public String getTypeString() { + return type.toString(); + } + + public static EmitterType typeOf( String typeStr ) { + if (typeStr.equals(EmitterType.MOBILE.toString())) + return EmitterType.MOBILE; + if (typeStr.equals(EmitterType.WLAN_24GHZ.toString())) + return EmitterType.WLAN_24GHZ; + if (typeStr.equals(EmitterType.WLAN_5GHZ.toString())) + return EmitterType.WLAN_5GHZ; + return EmitterType.INVALID; + } + + public String getId() { + return id; + } + + public RfIdentification getRfIdent() { + return new RfIdentification(id, type); + } + + public long getTrust() { + return trust; + } + + public double getLat() { + if (coverage != null) + return coverage.getCenter_lat(); + return 0.0; + } + + public double getLon() { + if (coverage != null) + return coverage.getCenter_lon(); + return 0.0; + } + + public double getRadius() { + if (coverage != null) + return coverage.getRadius(); + return 0.0; + } + + public double getRadiusNS() { + if (coverage != null) + return coverage.getRadius_ns(); + return 0.0; + } + + public double getRadiusEW() { + if (coverage != null) + return coverage.getRadius_ew(); + return 0.0; + } + + public void setLastObservation(Observation obs) { + mLastObservation = obs; + note = obs.getNote(); + } + + public void setNote(String n) { + if (!note.equals(n)) { + note = n; + if (blacklistEmitter()) + changeStatus(EmitterStatus.STATUS_BLACKLISTED, "initSelf()"); + } + } + + public String getNote() { + return note; + } + + /** + * All RfEmitter objects are managed through a cache. The cache needs ages out + * emitters that have not been seen (or used) in a while. To do that it needs + * to maintain age information for each RfEmitter object. Having the RfEmitter + * object itself store the cache age is a bit of a hack, but we do it anyway. + * + * @return The current cache age. + */ + public int getAge() { + return ageSinceLastUse; + } + + /** + * Resets the cache age to zero. + */ + public void resetAge() { + ageSinceLastUse = 0; + } + + /** + * Increment the cache age for this object. + */ + public void incrementAge() { + ageSinceLastUse++; + } + + /** + * Periodically the cache sync's all dirty objects to the flash database. + * This routine is called by the cache to determine if it needs to be sync'd. + * + * @return True if this RfEmitter needs to be written to flash. + */ + public boolean syncNeeded() { + return (status == EmitterStatus.STATUS_NEW) || + (status == EmitterStatus.STATUS_CHANGED) || + ((status == EmitterStatus.STATUS_BLACKLISTED) && + (coverage != null)); + } + + /** + * Synchronize this object to the flash based database. This method is called + * by the cache when it is an appropriate time to assure the flash based + * database is up to date with our current coverage, trust, etc. + * + * @param db The database we should write our data to. + */ + public void sync(Database db) { + EmitterStatus newStatus = status; + + Log.i(TAG, "sync('" + logString() + "'), status: " + status); + switch (status) { + case STATUS_UNKNOWN: + // Not in database, we have no location. Nothing to sync. + break; + + case STATUS_BLACKLISTED: + // If our coverage value is not null it implies that we exist in the + // database. If so we ought to remove the entry. + if (coverage != null) { + db.drop(this); + coverage = null; + Log.i(TAG, "sync('" + logString() + "') - Blacklisted dropping from database."); + } + break; + + case STATUS_NEW: + // Not in database, we have location. Add to database + db.insert(this); + newStatus = EmitterStatus.STATUS_CACHED; + Log.i(TAG, "sync('" + logString() + "') - Status new."); + break; + + case STATUS_CHANGED: + // In database but we have changes + if (trust < MINIMUM_TRUST) { + Log.i(TAG, "sync('" + logString() + "') - Trust below minimum, dropping from database."); + db.drop(this); + } else + db.update(this); + newStatus = EmitterStatus.STATUS_CACHED; + break; + + case STATUS_CACHED: + // In database but we don't have any changes + break; + } + changeStatus(newStatus, "sync('"+logString()+"')"); + + } + + public String logString() { + return "RF Emitter: Type=" + type + ", ID='" + id + "', Note='" + note + "'"; + } + + /** + * Given an emitter type, return the various characteristics we need to know + * to model it. + * + * @param t An emitter type (WLAN_24GHZ, MOBILE, etc.) + * @return The characteristics needed to model the emitter + */ + public static RfCharacteristics getRfCharacteristics(EmitterType t) { + switch (t) { + case WLAN_24GHZ: + // For 2.4 GHz, indoor range seems to be described as about 46 meters + // with outdoor range about 90 meters. Set the minimum range to be about + // 3/4 of the indoor range and the typical range somewhere between + // the indoor and outdoor ranges. + // However we've seem really, really long range detection in rural areas + // so base the move distance on that. + return new RfCharacteristics( + 20 * METERS, // reqdGpsAccuracy + 35 * METERS, // minimumRange + 65 * METERS, // typicalRange + 300 * METERS, // moveDetectDistance - Seen pretty long detection in very rural areas + 0, // discoveryTrust + REQUIRED_TRUST/3, // incrTrust + 1, // decrTrust + 2 // minCount + ); + + case WLAN_5GHZ: + // For 2.4 GHz, indoor range seems to be described as about 46 meters + // with outdoor range about 90 meters. Set the minimum range to be about + // 3/4 of the indoor range and the typical range somewhere between + // the indoor and outdoor ranges. + // However we've seem really, really long range detection in rural areas + // so base the move distance on that. + return new RfCharacteristics( + 20 * METERS, // reqdGpsAccuracy + 35 * METERS, // minimumRange + 65 * METERS, // typicalRange + 300 * METERS, // moveDetectDistance - Seen pretty long detection in very rural areas + 0, // discoveryTrust + REQUIRED_TRUST/3, // incrTrust + 1, // decrTrust + 2 // minCount + ); + + case MOBILE: + return new RfCharacteristics( + 100 * METERS, // reqdGpsAccuracy + 500 * METERS, // minimumRange + 2 * KM, // typicalRange + 100 * KM, // moveDetectDistance - In the desert towers cover large areas + MAXIMUM_TRUST, // discoveryTrust + MAXIMUM_TRUST, // incrTrust + 0, // decrTrust + 1 // minCount + ); + } + + // Unknown emitter type, just throw out some values that make it unlikely that + // we will ever use it (require too accurate a GPS location, never increment trust, etc.). + return new RfCharacteristics( + 2 * METERS, // reqdGpsAccuracy + 50 * METERS, // minimumRange + 50 * METERS, // typicalRange + 100 * METERS, // moveDetectDistance + 0, // discoveryTrust + 0, // incrTrust + 1, // decrTrust + 99 // minCount + ); + } + + /** + * Unfortunately some types of RF emitters are very mobile and a mobile emitter + * should not be used to estimate our position. Part of the way to deal with this + * issue is to maintain a trust metric. Trust has a maximum value, so when we + * are asked to increment trust we need to check that we have not passed the limit. + */ + public void incrementTrust() { + //Log.i(TAG, "incrementTrust('"+id+"') - entry."); + if (canUpdate()) { + long newTrust = trust + ourCharacteristics.incrTrust; + if (newTrust > MAXIMUM_TRUST) + newTrust = MAXIMUM_TRUST; + if (newTrust != trust) { + // Log.i(TAG, "incrementTrust('" + logString() + "') - trust change: " + trust + "->" + newTrust); + trust = newTrust; + changeStatus(EmitterStatus.STATUS_CHANGED, "incrementTrust('"+logString()+"')"); + } + } + } + + /** + * Decrease our trust of this emitter. This can happen because we expected to see it at our + * current location and didn't. + */ + public void decrementTrust() { + if (canUpdate()) { + long oldTrust = trust; + trust -= ourCharacteristics.decrTrust; + if (oldTrust != trust) { + // Log.i(TAG, "decrementTrust('" + logString() + "') - trust change: " + oldTrust + "->" + trust); + changeStatus(EmitterStatus.STATUS_CHANGED, "decrementTrust('" + logString() + "')"); + } + } + } + + /** + * When a scan first detects an emitter a RfEmitter object is created. But at that time + * no lookup of the saved information is needed or made. When appropriate, the database + * is checked for saved information about the emitter and this method is called to add + * that saved information to our model. + * + * @param emitterInfo Saved information about this emitter from the database. + */ + public void updateInfo(Database.EmitterInfo emitterInfo) { + if (emitterInfo != null) { + if (coverage == null) + coverage = new BoundingBox(emitterInfo); + //Log.i(TAG,"updateInfo() - Setting info for '"+id+"'"); + trust = emitterInfo.trust; + note = emitterInfo.note; + changeStatus(EmitterStatus.STATUS_CACHED, "updateInfo('"+logString()+"')"); + } + } + + /** + * Update our estimate of the coverage and location of the emitter based on a + * position report from the GPS system. + * + * @param gpsLoc A position report from a trusted (non RF emitter) source + */ + public void updateLocation(Location gpsLoc) { + + if (status == EmitterStatus.STATUS_BLACKLISTED) + return; + + if ((gpsLoc == null) || (gpsLoc.getAccuracy() > ourCharacteristics.reqdGpsAccuracy)) { + Log.i(TAG, "updateLocation("+logString()+") No GPS location or location inaccurate:" + ((gpsLoc != null) ? gpsLoc.getAccuracy() : "null") + ", " + ourCharacteristics.reqdGpsAccuracy); + return; + } + + if (coverage == null) { + Log.i(TAG, "updateLocation("+logString()+") emitter is new."); + coverage = new BoundingBox(gpsLoc.getLatitude(), gpsLoc.getLongitude(), 0.0f); + changeStatus(EmitterStatus.STATUS_NEW, "updateLocation('"+logString()+"') New"); + return; + } + + // Add the GPS sample to the known bounding box of the emitter. + + if (coverage.update(gpsLoc.getLatitude(), gpsLoc.getLongitude())) { + // Bounding box has increased, see if it is now unbelievably large + Log.i(TAG, "updateLocation("+id+") coverage: " + coverage.getRadius() + ", " + ourCharacteristics.moveDetectDistance); + if (coverage.getRadius() >= ourCharacteristics.moveDetectDistance) { + Log.i(TAG, "updateLocation("+id+") emitter has moved (" + gpsLoc.distanceTo(_getLocation()) + ")"); + coverage = new BoundingBox(gpsLoc.getLatitude(), gpsLoc.getLongitude(), 0.0f); + trust = ourCharacteristics.discoveryTrust; + changeStatus(EmitterStatus.STATUS_CHANGED, "updateLocation('"+logString()+"') Moved"); + } else { + changeStatus(EmitterStatus.STATUS_CHANGED, "updateLocation('" + logString() + "') BBOX update"); + } + } + } + + /** + * User facing location value. Differs from internal one in that we don't report + * locations that are guarded due to being new or moved. + * + * @return The coverage estimate for our RF emitter or null if we don't trust our + * information. + */ + public Location getLocation() { + // If we have no observation of the emitter we ought not give a + // position estimate based on it. + if (mLastObservation == null) + return null; + + // If we don't trust the location, we ought not give a position + // estimate based on it. + if ((trust < REQUIRED_TRUST) || (status == EmitterStatus.STATUS_BLACKLISTED)) + return null; + + // If we don't have a coverage estimate we will get back a null location + Location location = _getLocation(); + if (location == null) + return null; + + // If we are unbelievably close to null island, don't report location + if (!BackendService.notNullIsland(location)) + return null; + + // Time tags based on time of most recent observation + location.setTime(mLastObservation.getLastUpdateTimeMs()); + location.setElapsedRealtimeNanos(mLastObservation.getElapsedRealtimeNanos()); + + Bundle extras = new Bundle(); + extras.putString(LOC_RF_TYPE, type.toString()); + extras.putString(LOC_RF_ID, id); + extras.putInt(LOC_ASU,mLastObservation.getAsu()); + extras.putLong(LOC_MIN_COUNT, ourCharacteristics.minCount); + location.setExtras(extras); + return location; + } + + /** + * If we have any coverage information, returns an estimate of that coverage. + * For convenience, we use the standard Location record as it contains a center + * point and radius (accuracy). + * + * @return Coverage estimate for emitter or null it does not exist. + */ + private Location _getLocation() { + if (coverage == null) + return null; + + final Location location = new Location(BackendService.LOCATION_PROVIDER); + + location.setLatitude(coverage.getCenter_lat()); + location.setLongitude(coverage.getCenter_lon()); + + // Hard limit the minimum accuracy based on the type of emitter + location.setAccuracy((float)Math.max(this.getRadius(),ourCharacteristics.minimumRange)); + + return location; + } + + /** + * As part of our effort to not use mobile emitters in estimating or location + * we blacklist ones that match observed patterns. + * + * @return True if the emitter is blacklisted (should not be used in position computations). + */ + private boolean blacklistEmitter() { + switch (this.type) { + case WLAN_24GHZ: + case WLAN_5GHZ: + return blacklistWifi(); + + case MOBILE: + return false; // Not expecting mobile towers to move around. + + } + return false; + } + + /** + * Checks the note field (where the SSID is saved) to see if it appears to be + * an AP that is likely to be moving. Typical checks are to see if substrings + * in the SSID match that of cell phone manufacturers or match known patterns + * for public transport (busses, trains, etc.) or in car WLAN defaults. + * + * @return True if emitter should be blacklisted. + */ + private boolean blacklistWifi() { + final String lc = note.toLowerCase(Locale.US); + + // Seen a large number of WiFi networks where the SSID is the last + // three octets of the MAC address. Often in rural areas where the + // only obvious source would be other automobiles. So suspect that + // this is the default setup for a number of vehicle manufactures. + final String macSuffix = id.substring(id.length()-8).toLowerCase(Locale.US).replace(":", ""); + boolean rslt = + // Mobile phone brands + lc.contains("android") || // mobile tethering + lc.startsWith("HUAWEI-") || + lc.contains("ipad") || // mobile tethering + lc.contains("iphone") || // mobile tethering + lc.contains("motorola") || // mobile tethering + lc.endsWith(" phone") || // "Lans Phone" seen + lc.startsWith("moto ") || // "Moto E (4) 9509" seen + note.startsWith("MOTO") || // "MOTO9564" and "MOTO9916" seen + note.startsWith("Samsung Galaxy") || // mobile tethering + lc.startsWith("lg aristo") || // "LG Aristo 7124" seen + + // Mobile network brands + lc.contains("mobile hotspot") || // e.g "MetroPCS Portable Mobile Hotspot" + note.startsWith("CellSpot") || // T-Mobile US portable cell based WiFi + note.startsWith("Verizon-") || // Verizon mobile hotspot + + // Per some instructional videos on YouTube, recent (2015 and later) + // General Motors built vehicles come with a default WiFi SSID of the + // form "WiFi Hotspot 1234" where the 1234 is different for each car. + // The SSID can be changed but the recommended SSID to change to + // is of the form "first_name vehicle_model" (e.g. "Bryces Silverado"). + lc.startsWith("wifi hotspot ") || // Default GM vehicle WiFi name + lc.endsWith("corvette") || // Chevy Corvette. "TS Corvette" seen. + lc.endsWith("silverado") || // GMC Silverado. "Bryces Silverado" seen. + lc.endsWith("chevy") || // Chevrolet. "Davids Chevy" seen + lc.endsWith("truck") || // "Morgans Truck" and "Wally Truck" seen + lc.endsWith("suburban") || // Chevy/GMC Suburban. "Laura Suburban" seen + lc.endsWith("terrain") || // GMC Terrain. "Nelson Terrain" seen + lc.endsWith("sierra") || // GMC pickup. "dees sierra" seen + + // Per an instructional video on YouTube, recent (2014 and later) Chrysler-Fiat + // vehicles have a SSID of the form "Chrysler uconnect xxxxxx" where xxxxxx + // seems to be a hex digit string (suffix of BSSID?). + lc.contains(" uconnect ") || // Chrysler built vehicles + + // Per instructional video on YouTube, Mercedes cars have and SSID of + // "MB WLAN nnnnn" where nnnnn is a 5 digit number. + lc.startsWith("mb wlan ") || // Mercedes + + // Other automobile manufactures default naming + + lc.equals(macSuffix) || // Apparent default SSID name for many cars + note.startsWith("Audi") || // some cars seem to have this AP on-board + note.startsWith("Chevy ") || // "Chevy Cruz 7774" seen. + note.startsWith("GMC WiFi") || // General Motors + note.startsWith("MyVolvo") || // Volvo in car WiFi + + // Transit agencies + lc.startsWith("oebb ") || // WLAN network on Austrian Oebb trains + lc.startsWith("westbahn ") || // WLAN network on Austrian Westbahn trains + lc.contains("admin@ms ") || // WLAN network on Hurtigruten ships + lc.contains("contiki-wifi") || // WLAN network on board of bus + lc.contains("db ic bus") || // WLAN network on board of German bus + lc.contains("deinbus.de") || // WLAN network on board of German bus + lc.contains("ecolines") || // WLAN network on board of German bus + lc.contains("eurolines_wifi") || // WLAN network on board of German bus + lc.contains("fernbus") || // WLAN network on board of German bus + lc.contains("flixbus") || // WLAN network on board of German bus + lc.contains("guest@ms ") || // WLAN network on Hurtigruten ships + lc.contains("muenchenlinie") || // WLAN network on board of bus + lc.contains("postbus") || // WLAN network on board of bus line + lc.contains("telekom_ice") || // WLAN network on DB trains + lc.contains("skanetrafiken") || // WLAN network on Skånetrafiken (Sweden) buses and trains + lc.contains("oresundstag") || // WLAN network on Øresundståg (Sweden/Denmark) trains + lc.contentEquals("amtrak") || // WLAN network on USA Amtrak trains + lc.contentEquals("amtrakconnect") || // WLAN network on USA Amtrak trains + lc.contentEquals("CDWiFi") || // WLAN network on Czech railways + lc.contentEquals("megabus") || // WLAN network on MegaBus US bus + lc.contentEquals("Regiojet - zluty") || // WLAN network on Czech airline + lc.contentEquals("RegioJet - zluty") || // WLAN network on Czech airline + lc.contentEquals("WESTlan") || // WLAN network on Austrian railways + lc.contentEquals("Wifi in de trein") || // WLAN network on Dutch railway + note.startsWith("BusWiFi") || // Some transit buses in LA Calif metro area + note.startsWith("CoachAmerica") || // Charter bus service with on board WiFi + note.startsWith("DisneyLandResortExpress") || // Bus with on board WiFi + note.startsWith("TaxiLinQ") || // Taxi cab wifi system. + note.startsWith("TransitWirelessWiFi") || // New York City public transport wifi + + // Dash cams + note.startsWith("YICarCam") || // Dashcam WiFi. + + // Other + lc.contains("mobile") || // What I'd put into a mobile hotspot name + lc.contains("nsb_interakti") || // ??? + lc.contains("NVRAM WARNING") // NVRAM WARNING Error pseudo-network + + // lc.endsWith("_nomap") // Google unsubscibe option + ; + //if (rslt) + // Log.i(TAG, "blacklistWifi('" + logString() + "') blacklisted."); + return rslt; + } + + /** + * Only some types of emitters can be updated when a GPS position is received. A + * simple check but done in a couple places so extracted out to this routine so that + * we are consistent in how we check things. + * + * @return True if coverage and/or trust can be updated. + */ + private boolean canUpdate() { + boolean rslt = true; + switch (status) { + case STATUS_BLACKLISTED: + case STATUS_UNKNOWN: + rslt = false; + break; + } + return rslt; + } + + /** + * Our status can only make a small set of allowed transitions. Basically a simple + * state machine. To assure our transistions are all legal, this routine is used for + * all changes. + * + * @param newStatus The desired new status (state) + * @param info Logging information for debug purposes + */ + private void changeStatus( EmitterStatus newStatus, String info) { + if (newStatus == status) + return; + + EmitterStatus finalStatus = status; + switch (finalStatus) { + case STATUS_BLACKLISTED: + // Once blacklisted cannot change. + break; + + case STATUS_CACHED: + case STATUS_CHANGED: + switch (newStatus) { + case STATUS_BLACKLISTED: + case STATUS_CACHED: + case STATUS_CHANGED: + finalStatus = newStatus; + break; + } + break; + + case STATUS_NEW: + switch (newStatus) { + case STATUS_BLACKLISTED: + case STATUS_CACHED: + finalStatus = newStatus; + break; + } + break; + + case STATUS_UNKNOWN: + switch (newStatus) { + case STATUS_BLACKLISTED: + case STATUS_CACHED: + case STATUS_NEW: + finalStatus = newStatus; + } + break; + } + + //Log.i(TAG,"changeStatus("+newStatus+", "+ info + ") " + status + " -> " + finalStatus); + status = finalStatus; + } +} diff --git a/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/database/RfIdentification.java b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/database/RfIdentification.java new file mode 100644 index 0000000..e6ca85b --- /dev/null +++ b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/database/RfIdentification.java @@ -0,0 +1,120 @@ +package org.microg.nlp.backend.dejavu.database; + +/* + * DejaVu - A location provider backend for microG/UnifiedNlp + * + * Copyright (C) 2017 Tod Fitch + * + * 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. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * Created by tfitch on 10/4/17. + */ + +import java.math.BigInteger; +import java.security.*; +import android.util.Log; + +/** + * This class forms a complete identification for a RF emitter. + * + * All it has are two fields: A rfID string that must be unique within a type + * or class of emitters. And a rtType value that indicates the type of RF + * emitter we are dealing with. + */ + +public class RfIdentification implements Comparable{ + private static final String TAG = "DejaVu RfIdent"; + + private final String rfId; + private final RfEmitter.EmitterType rfType; + private final String uniqueId; + + RfIdentification(String id, RfEmitter.EmitterType t) { + rfId = id; + rfType = t; + uniqueId = genUniqueId(rfType, rfId); + } + + public int compareTo(RfIdentification o) { + return uniqueId.compareTo(o.uniqueId); + } + + public boolean equals(Object o) { + if (this == o) + return true; + if ( !(o instanceof RfIdentification)) + return false; + + RfIdentification that = (RfIdentification)o; + return (uniqueId.equals(that.uniqueId)); + } + + public String getRfId() { + return rfId; + } + + public RfEmitter.EmitterType getRfType() { + return rfType; + } + + public String getUniqueId() { + return uniqueId; + } + + /** + * Return a hash code for Android to determine if we are like + * some other object. Since we already have a unique ID computed + * for our database records, use that but turn it into the int + * expected by Android. + * + * @return Int Android hash code + */ + public int hashCode() { + return uniqueId.hashCode(); + } + + public String toString() { + return "rfId=" + rfId + ", rfType=" + rfType; + } + + /** + * Generate a unique string for our RF identification. Using MD5 as it + * ought not have collisions but is relatively cheap to compute. Since + * we aren't doing cryptography here we need not worry about it being + * a secure hash. + * + * @param rfType The type of emitter + * @param rfIdent The ID string unique to the type of emitter + * @return String A unique identification string + */ + private String genUniqueId(RfEmitter.EmitterType rfType, String rfIdent) { + String hashtext = rfType + ":" + rfIdent; + try { + byte[] bytes = hashtext.getBytes("UTF-8"); + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(bytes); + BigInteger bigInt = new BigInteger(1,digest); + hashtext = bigInt.toString(16); + while(hashtext.length() < 32 ){ + hashtext = "0"+hashtext; + } + } catch (Exception e) { + Log.d(TAG, "genUniqueId(): Exception" + e.getMessage()); + } + return hashtext; + } + +} diff --git a/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/ui/MainActivity.java b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/ui/MainActivity.java new file mode 100644 index 0000000..170245c --- /dev/null +++ b/app-dejavu/src/main/java/org/microg/nlp/backend/dejavu/ui/MainActivity.java @@ -0,0 +1,109 @@ +package org.microg.nlp.backend.dejavu.ui; + +import android.Manifest; +import android.content.DialogInterface; +import android.os.Build; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import org.microg.nlp.backend.dejavu.R; +import com.google.android.material.snackbar.Snackbar; + +import org.microg.nlp.backend.dejavu.database.Database; +import org.microg.nlp.backend.dejavu.database.RfEmitter; + +import java.util.Map; + +public class MainActivity extends AppCompatActivity { + + private static final String TAG = "MainActivity"; + + private TextView dbNumberOfRows; + private Button deleteDB; + + private Database db; + + private final static int BACKGROUND_LOCATION_PERMISSION_CODE = 333; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTheme(androidx.appcompat.R.style.Theme_AppCompat); + setContentView(R.layout.activity_main); + + if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.ACCESS_BACKGROUND_LOCATION)) { + String message = "Background"; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + message += getPackageManager().getBackgroundPermissionOptionLabel(); + } + new AlertDialog.Builder(MainActivity.this) + .setTitle(message) + .setMessage(message) + .setPositiveButton("OK", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + ActivityCompat.requestPermissions(MainActivity.this, + new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, BACKGROUND_LOCATION_PERMISSION_CODE); + } + }) + .setNegativeButton("Dismiss", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Snackbar.make(findViewById(android.R.id.content), "Not granted", Snackbar.LENGTH_SHORT).show(); + } + }) + .create().show(); + } else { + ActivityCompat.requestPermissions(MainActivity.this, + new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, BACKGROUND_LOCATION_PERMISSION_CODE); + } + + db = new Database(this); + + dbNumberOfRows = findViewById(R.id.db_number_of_rows); + dbNumberOfRows.setText(String.valueOf(db.getRowsCount())); + + deleteDB = findViewById(R.id.delete_database); + deleteDB.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + db.clearDatabase(); + } + }); + } + + @Override + public void onResume() { + super.onResume(); + } + + @Override + protected void onPause() { + super.onPause(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + } + + return super.onOptionsItemSelected(item); + } +} diff --git a/app-dejavu/src/main/res/layout/activity_main.xml b/app-dejavu/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..0e097e0 --- /dev/null +++ b/app-dejavu/src/main/res/layout/activity_main.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + +