feat: Add Wear OS Interface for GPSTest (#605)

* Add Wear OS Interface GPSTest

* Change the module and package name to wear

* Refactor packages to gpstest

* Change Time format

* Implement library in gpstest

* Move common data from app to library with cross-module dependency injections failures

* Solve the cross-module dependency injections problem about context

* Watch face with flashed problem

* Implement progress bar

* Add permissions request

* Resolve column overlap

* Add progress bar for Wear OS

* Update data from location on Wear OS

* Update "# Sats", "PDOP" and "H/V Dop" on Wear OS.

* Update timestamp from location.

* fix: OSMDroid build flavor

* Fix test bugs.

Co-authored-by: barbeau <barbeaus@google.com>
This commit is contained in:
Ada
2023-01-10 22:55:22 +08:00
committed by GitHub
parent b4f18fc345
commit 7f56a0e849
323 changed files with 3148 additions and 2073 deletions

View File

@@ -4,7 +4,7 @@ apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
compileSdkVersion 31
compileSdkVersion 33
defaultConfig {
minSdkVersion 24
@@ -104,6 +104,9 @@ android {
}
}
}
dataBinding {
enabled = true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -119,10 +122,11 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion '1.1.0-beta02'
kotlinCompilerExtensionVersion '1.1.0-rc02'
}
buildFeatures {
dataBinding true
viewBinding true
}
@@ -167,6 +171,7 @@ dependencies {
// Multidex - Needed for APIs < 21
implementation 'androidx.multidex:multidex:2.0.1'
implementation project(path: ':library')
// To observe flows via co-routines within the Service
def lifecycle_version = "2.4.0-rc01"
@@ -195,7 +200,7 @@ dependencies {
// Integration with activities
implementation 'androidx.activity:activity-compose:1.4.0'
implementation "androidx.compose.compiler:compiler:1.1.0-beta02"
implementation "androidx.compose.compiler:compiler:1.1.0-rc02"
// Compose Material Design
implementation 'androidx.compose.material:material:1.0.5'
// Bridging XML themes to Compose
@@ -205,7 +210,7 @@ dependencies {
// Tooling support (Previews, etc.)
implementation 'androidx.compose.ui:ui-tooling:1.0.5'
// Integration with ViewModels
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'
//Integration with LiveData
implementation 'androidx.compose.runtime:runtime-livedata:1.1.0-beta02'
// UI Tests

View File

@@ -18,12 +18,12 @@ package com.android.gpstest
import android.os.Build
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import com.android.gpstest.model.GnssType
import com.android.gpstest.model.SatelliteStatus
import com.android.gpstest.model.SbasType
import com.android.gpstest.util.CarrierFreqUtils
import com.android.gpstest.util.CarrierFreqUtils.CF_UNKNOWN
import com.android.gpstest.util.CarrierFreqUtils.CF_UNSUPPORTED
import com.android.gpstest.library.model.GnssType
import com.android.gpstest.library.model.SatelliteStatus
import com.android.gpstest.library.model.SbasType
import com.android.gpstest.library.util.CarrierFreqUtils
import com.android.gpstest.library.util.CarrierFreqUtils.CF_UNKNOWN
import com.android.gpstest.library.util.CarrierFreqUtils.CF_UNSUPPORTED
import junit.framework.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue

View File

@@ -20,14 +20,14 @@ import android.location.Location
import android.os.Build
import androidx.test.filters.SdkSuppress
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import com.android.gpstest.model.GnssType
import com.android.gpstest.model.Orientation
import com.android.gpstest.model.SatelliteStatus
import com.android.gpstest.util.FormatUtils.toLog
import com.android.gpstest.util.IOUtils
import com.android.gpstest.util.SatelliteUtil.isBearingAccuracySupported
import com.android.gpstest.util.SatelliteUtil.isSpeedAccuracySupported
import com.android.gpstest.util.SatelliteUtil.isVerticalAccuracySupported
import com.android.gpstest.library.model.GnssType
import com.android.gpstest.library.model.Orientation
import com.android.gpstest.library.model.SatelliteStatus
import com.android.gpstest.library.util.FormatUtils.toLog
import com.android.gpstest.library.util.IOUtils
import com.android.gpstest.library.util.SatelliteUtil.isBearingAccuracySupported
import com.android.gpstest.library.util.SatelliteUtil.isSpeedAccuracySupported
import com.android.gpstest.library.util.SatelliteUtil.isVerticalAccuracySupported
import junit.framework.Assert
import org.junit.Assert.assertEquals
import org.junit.Test

View File

@@ -17,8 +17,9 @@ package com.android.gpstest
import android.content.Intent
import android.location.Location
import androidx.test.InstrumentationRegistry.getTargetContext
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import com.android.gpstest.util.IOUtils
import com.android.gpstest.library.util.IOUtils
import junit.framework.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
@@ -37,11 +38,11 @@ class IOUtilsTest {
fun testIsShowRadarIntent() {
// SHOW_RADAR intent
val intent = Intent("com.google.android.radar.SHOW_RADAR")
assertTrue(IOUtils.isShowRadarIntent(intent))
assertTrue(IOUtils.isShowRadarIntent(getTargetContext(),intent))
// Not SHOW_RADAR intents
assertFalse(IOUtils.isShowRadarIntent(Intent("not.show.radar.intent")))
assertFalse(IOUtils.isShowRadarIntent(null));
assertFalse(IOUtils.isShowRadarIntent(getTargetContext(),Intent("not.show.radar.intent")))
assertFalse(IOUtils.isShowRadarIntent(getTargetContext(),null));
}
/**
@@ -58,7 +59,7 @@ class IOUtilsTest {
intent.putExtra("latitude", 28.0527222f)
intent.putExtra("longitude", -82.4331001f)
val location = IOUtils.getLocationFromIntent(intent)
val location = IOUtils.getLocationFromIntent(getTargetContext(),intent)
assertEquals(28.0527222, location.latitude, delta)
assertEquals(-82.433100, location.longitude, delta)
assertFalse(location.hasAltitude())
@@ -69,7 +70,7 @@ class IOUtilsTest {
intentWithAltitude.putExtra("longitude", -82.4331001f)
intentWithAltitude.putExtra("altitude", 20.3f)
val locationWithAltitude = IOUtils.getLocationFromIntent(intentWithAltitude)
val locationWithAltitude = IOUtils.getLocationFromIntent(getTargetContext(),intentWithAltitude)
assertEquals(28.0527222, locationWithAltitude.latitude, delta)
assertEquals(-82.433100, locationWithAltitude.longitude, delta)
assertEquals(20.3, locationWithAltitude.altitude, delta)
@@ -79,7 +80,7 @@ class IOUtilsTest {
intentDouble.putExtra("latitude", 28.0527222)
intentDouble.putExtra("longitude", -82.4331001)
val locationDouble = IOUtils.getLocationFromIntent(intentDouble)
val locationDouble = IOUtils.getLocationFromIntent(getTargetContext(), intentDouble)
assertEquals(28.0527222, locationDouble.latitude, delta)
assertEquals(-82.433100, locationDouble.longitude, delta)
assertFalse(locationDouble.hasAltitude())
@@ -90,7 +91,7 @@ class IOUtilsTest {
intentDoubleWithAltitude.putExtra("longitude", -82.4331001)
intentDoubleWithAltitude.putExtra("altitude", 20.3)
val locationDoubleWithAltitude = IOUtils.getLocationFromIntent(intentDoubleWithAltitude)
val locationDoubleWithAltitude = IOUtils.getLocationFromIntent(getTargetContext(), intentDoubleWithAltitude)
assertEquals(28.0527222, locationDoubleWithAltitude.latitude, delta)
assertEquals(-82.433100, locationDoubleWithAltitude.longitude, delta)
assertEquals(20.3, locationDoubleWithAltitude.altitude, delta)
@@ -101,7 +102,7 @@ class IOUtilsTest {
intentNullAltitude.putExtra("longitude", -82.4331001)
intentNullAltitude.putExtra("altitude", Double.NaN)
val locationNullAltitude = IOUtils.getLocationFromIntent(intentNullAltitude)
val locationNullAltitude = IOUtils.getLocationFromIntent(getTargetContext(), intentNullAltitude)
assertEquals(28.0527222, locationNullAltitude.latitude, delta)
assertEquals(-82.433100, locationNullAltitude.longitude, delta)
assertFalse(locationNullAltitude.hasAltitude())
@@ -112,7 +113,7 @@ class IOUtilsTest {
intentDoubleWithFloatAltitude.putExtra("longitude", -82.4331001)
intentDoubleWithFloatAltitude.putExtra("altitude", 20.3f)
val locationDoubleWithFloatAltitude = IOUtils.getLocationFromIntent(intentDoubleWithFloatAltitude)
val locationDoubleWithFloatAltitude = IOUtils.getLocationFromIntent(getTargetContext(),intentDoubleWithFloatAltitude)
assertEquals(28.0527222, locationDoubleWithFloatAltitude.latitude, delta)
assertEquals(-82.433100, locationDoubleWithFloatAltitude.longitude, delta)
assertEquals(20.3, locationDoubleWithFloatAltitude.altitude, delta)
@@ -123,12 +124,12 @@ class IOUtilsTest {
*/
@Test
fun testCreateShowRadarIntent() {
val resultNoAltitude = IOUtils.createShowRadarIntent(24.5253, 87.23434, null)
val resultNoAltitude = IOUtils.createShowRadarIntent(getTargetContext(),24.5253, 87.23434, null)
assertEquals(24.5253, resultNoAltitude?.extras?.get("latitude"))
assertEquals(87.23434, resultNoAltitude?.extras?.get("longitude"))
assertFalse(resultNoAltitude.hasExtra("altitude"))
val resultWithAltitude = IOUtils.createShowRadarIntent(24.5253, 87.23434, 15.5)
val resultWithAltitude = IOUtils.createShowRadarIntent(getTargetContext(),24.5253, 87.23434, 15.5)
assertEquals(24.5253, resultWithAltitude.extras?.get("latitude"))
assertEquals(87.23434, resultWithAltitude.extras?.get("longitude"))
assertEquals(15.5, resultWithAltitude.extras?.get("altitude"))
@@ -137,7 +138,7 @@ class IOUtilsTest {
locationNoAltitude.latitude = -20.8373
locationNoAltitude.longitude = -120.8273
val resultFromLocationNoAltitude = IOUtils.createShowRadarIntent(locationNoAltitude)
val resultFromLocationNoAltitude = IOUtils.createShowRadarIntent(getTargetContext(),locationNoAltitude)
assertEquals(-20.8373, resultFromLocationNoAltitude?.extras?.get("latitude"))
assertEquals(-120.8273, resultFromLocationNoAltitude?.extras?.get("longitude"))
assertFalse(resultNoAltitude.hasExtra("altitude"))
@@ -147,7 +148,7 @@ class IOUtilsTest {
locationWithAltitude.longitude = -126.8273
locationWithAltitude.altitude = -13.5
val resultFromLocationWithAltitude = IOUtils.createShowRadarIntent(locationWithAltitude)
val resultFromLocationWithAltitude = IOUtils.createShowRadarIntent(getTargetContext(),locationWithAltitude)
assertEquals(-26.8373, resultFromLocationWithAltitude.extras?.get("latitude"))
assertEquals(-126.8273, resultFromLocationWithAltitude.extras?.get("longitude"))
assertEquals(-13.5, resultFromLocationWithAltitude.extras?.get("altitude"))
@@ -160,60 +161,60 @@ class IOUtilsTest {
@Test
fun testGetLocationFromGeoUri() {
val geoUriLatLon = "geo:37.786971,-122.399677"
val result1 = IOUtils.getLocationFromGeoUri(geoUriLatLon)
val result1 = IOUtils.getLocationFromGeoUri(getTargetContext(),geoUriLatLon)
assertEquals(37.786971, result1.latitude)
assertEquals(-122.399677, result1.longitude)
assertFalse(result1.hasAltitude())
val geoUriLatLonAlt = "geo:-28.9876,87.1937,15"
val result2 = IOUtils.getLocationFromGeoUri(geoUriLatLonAlt)
val result2 = IOUtils.getLocationFromGeoUri(getTargetContext(),geoUriLatLonAlt)
assertEquals(-28.9876, result2.latitude)
assertEquals(87.1937, result2.longitude)
assertEquals(15.0, result2.altitude)
val geoUriLatLonZoom = "geo:-28.9876,87.1937?z=14"
val resultWithZoom = IOUtils.getLocationFromGeoUri(geoUriLatLonZoom)
val resultWithZoom = IOUtils.getLocationFromGeoUri(getTargetContext(),geoUriLatLonZoom)
assertEquals(-28.9876, resultWithZoom.latitude)
assertEquals(87.1937, resultWithZoom.longitude)
assertFalse(resultWithZoom.hasAltitude())
val geoUriLatLonAltZoom = "geo:-28.9876,87.1937,15?z=14"
val resultWithAltZoom = IOUtils.getLocationFromGeoUri(geoUriLatLonAltZoom)
val resultWithAltZoom = IOUtils.getLocationFromGeoUri(getTargetContext(), geoUriLatLonAltZoom)
assertEquals(-28.9876, resultWithAltZoom.latitude)
assertEquals(87.1937, resultWithAltZoom.longitude)
assertEquals(15.0, resultWithAltZoom.altitude)
assertTrue(resultWithAltZoom.hasAltitude())
val geoUriLatLonCrs = "geo:32.3482,43.06480;crs=EPSG:32618"
val resultLatLonCrs = IOUtils.getLocationFromGeoUri(geoUriLatLonCrs)
val resultLatLonCrs = IOUtils.getLocationFromGeoUri(getTargetContext(),geoUriLatLonCrs)
assertEquals(32.3482, resultLatLonCrs.latitude)
assertEquals(43.06480, resultLatLonCrs.longitude)
assertFalse(resultLatLonCrs.hasAltitude())
val geoUriLatLonAltCrs = "geo:32.3482,43.06480,15;crs=EPSG:32618"
val resultLatLonAltCrs = IOUtils.getLocationFromGeoUri(geoUriLatLonAltCrs)
val resultLatLonAltCrs = IOUtils.getLocationFromGeoUri(getTargetContext(),geoUriLatLonAltCrs)
assertEquals(32.3482, resultLatLonAltCrs.latitude)
assertEquals(43.06480, resultLatLonAltCrs.longitude)
assertEquals(15.0, resultLatLonAltCrs.altitude)
assertTrue(resultLatLonAltCrs.hasAltitude())
val invalidGeoUri = "http://not.a.geo.uri"
val result3 = IOUtils.getLocationFromGeoUri(invalidGeoUri)
val result3 = IOUtils.getLocationFromGeoUri(getTargetContext(), invalidGeoUri)
assertNull(result3)
val invalidLatLon = "geo:-999.9876,999.1937"
val result4 = IOUtils.getLocationFromGeoUri(invalidLatLon)
val result4 = IOUtils.getLocationFromGeoUri(getTargetContext(),invalidLatLon)
assertNull(result4)
val result5 = IOUtils.getLocationFromGeoUri(null)
val result5 = IOUtils.getLocationFromGeoUri(getTargetContext(), null)
assertNull(result5)
val invalidData2 = ""
val result6 = IOUtils.getLocationFromGeoUri(invalidData2)
val result6 = IOUtils.getLocationFromGeoUri(getTargetContext(), invalidData2)
assertNull(result6)
val invalidGeoUri2 = "http://not,a,geo,uri"
val result7 = IOUtils.getLocationFromGeoUri(invalidGeoUri2)
val result7 = IOUtils.getLocationFromGeoUri(getTargetContext(),invalidGeoUri2)
assertNull(result7)
}
@@ -225,17 +226,17 @@ class IOUtilsTest {
val l = Location("geouri-no-alt")
l.latitude = 28.12345
l.longitude = -82.1345
val geoUri = IOUtils.createGeoUri(l, true)
val geoUri = IOUtils.createGeoUri(getTargetContext(), l, true)
assertEquals("geo:28.12345,-82.1345", geoUri)
val lAlt = Location("geouri-with-alt")
lAlt.latitude = 28.12345
lAlt.longitude = -82.1345
lAlt.altitude = 104.2
val geoUriWithAlt = IOUtils.createGeoUri(lAlt, true)
val geoUriWithAlt = IOUtils.createGeoUri(getTargetContext(),lAlt, true)
assertEquals("geo:28.12345,-82.1345,104.2", geoUriWithAlt)
val geoUriAltExcluded = IOUtils.createGeoUri(lAlt, false)
val geoUriAltExcluded = IOUtils.createGeoUri(getTargetContext(), lAlt, false)
assertEquals("geo:28.12345,-82.1345", geoUriAltExcluded)
}

View File

@@ -20,7 +20,7 @@ import android.content.res.Resources;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
import com.android.gpstest.util.LocationUtils;
import com.android.gpstest.library.util.LocationUtils;
import org.junit.Test;
import org.junit.runner.RunWith;

View File

@@ -17,7 +17,7 @@ package com.android.gpstest;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
import com.android.gpstest.util.MathUtils;
import com.android.gpstest.library.util.MathUtils;
import org.junit.Test;
import org.junit.runner.RunWith;

View File

@@ -18,10 +18,10 @@ package com.android.gpstest
import android.location.GnssMeasurement.*
import android.os.Build
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import com.android.gpstest.model.GnssType
import com.android.gpstest.model.SatelliteStatus
import com.android.gpstest.model.SbasType
import com.android.gpstest.util.SatelliteUtils
import com.android.gpstest.library.model.GnssType
import com.android.gpstest.library.model.SatelliteStatus
import com.android.gpstest.library.model.SbasType
import com.android.gpstest.library.util.SatelliteUtils
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith

View File

@@ -17,14 +17,17 @@
package com.android.gpstest
import android.app.Application
import android.content.Context
import android.os.Build
import android.preference.PreferenceManager
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.InstrumentationRegistry
import androidx.test.InstrumentationRegistry.getTargetContext
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import com.android.gpstest.data.*
import com.android.gpstest.model.GnssType
import com.android.gpstest.model.SbasType
import com.android.gpstest.ui.SignalInfoViewModel
import com.android.gpstest.library.data.*
import com.android.gpstest.library.model.GnssType
import com.android.gpstest.library.model.SbasType
import com.android.gpstest.library.ui.SignalInfoViewModel
import kotlinx.coroutines.GlobalScope
import org.junit.Assert.*
import org.junit.Rule
@@ -39,13 +42,13 @@ class SignalInfoViewModelTest {
val instantTaskExecutorRule = InstantTaskExecutorRule()
private val repository = LocationRepository(
SharedLocationManager(InstrumentationRegistry.getTargetContext().applicationContext, GlobalScope),
SharedGnssStatusManager(InstrumentationRegistry.getTargetContext().applicationContext, GlobalScope),
SharedNmeaManager(InstrumentationRegistry.getTargetContext().applicationContext, GlobalScope),
SharedSensorManager(InstrumentationRegistry.getTargetContext().applicationContext, GlobalScope),
SharedNavMessageManager(InstrumentationRegistry.getTargetContext().applicationContext, GlobalScope),
SharedGnssMeasurementManager(InstrumentationRegistry.getTargetContext().applicationContext, GlobalScope),
SharedAntennaManager(InstrumentationRegistry.getTargetContext().applicationContext, GlobalScope)
SharedLocationManager(InstrumentationRegistry.getTargetContext().applicationContext, GlobalScope, PreferenceManager.getDefaultSharedPreferences(getTargetContext())),
SharedGnssStatusManager(InstrumentationRegistry.getTargetContext().applicationContext, GlobalScope, PreferenceManager.getDefaultSharedPreferences(getTargetContext())),
SharedNmeaManager(InstrumentationRegistry.getTargetContext().applicationContext, GlobalScope, PreferenceManager.getDefaultSharedPreferences(getTargetContext())),
SharedSensorManager(PreferenceManager.getDefaultSharedPreferences(getTargetContext()),InstrumentationRegistry.getTargetContext().applicationContext, GlobalScope),
SharedNavMessageManager(InstrumentationRegistry.getTargetContext().applicationContext, GlobalScope, PreferenceManager.getDefaultSharedPreferences(getTargetContext())),
SharedGnssMeasurementManager(PreferenceManager.getDefaultSharedPreferences(getTargetContext()), InstrumentationRegistry.getTargetContext().applicationContext, GlobalScope),
SharedAntennaManager(InstrumentationRegistry.getTargetContext().applicationContext, GlobalScope, PreferenceManager.getDefaultSharedPreferences(getTargetContext()))
)
/**
@@ -53,12 +56,13 @@ class SignalInfoViewModelTest {
*/
@Test
fun testDeviceInfoViewModel() {
val modelEmpty = SignalInfoViewModel(InstrumentationRegistry.getTargetContext().applicationContext as Application, repository)
modelEmpty.updateStatus(emptyList())
val context = getTargetContext()
val modelEmpty = SignalInfoViewModel(context, context.applicationContext as Application, repository, PreferenceManager.getDefaultSharedPreferences(context))
modelEmpty.updateStatus(context,emptyList(), PreferenceManager.getDefaultSharedPreferences(context))
// Test GPS L1 - should be 1 satellite, no L5 or dual-frequency
val modelGpsL1 = SignalInfoViewModel(InstrumentationRegistry.getTargetContext().applicationContext as Application, repository)
modelGpsL1.updateStatus(listOf(gpsL1(1, true)))
val modelGpsL1 = SignalInfoViewModel(context, InstrumentationRegistry.getTargetContext().applicationContext as Application, repository, PreferenceManager.getDefaultSharedPreferences(context))
modelGpsL1.updateStatus(context, listOf(gpsL1(1, true)), PreferenceManager.getDefaultSharedPreferences(getTargetContext()))
assertEquals(1, modelGpsL1.filteredGnssSatellites.value?.size)
assertFalse(modelGpsL1.isNonPrimaryCarrierFreqInView)
assertFalse(modelGpsL1.isNonPrimaryCarrierFreqInUse)
@@ -84,7 +88,7 @@ class SignalInfoViewModelTest {
modelGpsL1.reset();
// Test GPS L1 no signal - should be 1 satellite, no L5 or dual-frequency
modelGpsL1.updateStatus(listOf(gpsL1NoSignal(1)))
modelGpsL1.updateStatus(context, listOf(gpsL1NoSignal(1)), PreferenceManager.getDefaultSharedPreferences(context))
assertEquals(1, modelGpsL1.filteredGnssSatellites.value?.size)
assertFalse(modelGpsL1.isNonPrimaryCarrierFreqInView)
assertFalse(modelGpsL1.isNonPrimaryCarrierFreqInUse)
@@ -109,8 +113,8 @@ class SignalInfoViewModelTest {
// Test GPS L1 + L5 same sv - should be 1 satellite, dual frequency in view and but not in use
val modelGpsL1L5 = SignalInfoViewModel(InstrumentationRegistry.getTargetContext().applicationContext as Application, repository)
modelGpsL1L5.updateStatus(listOf(gpsL1(1, false), gpsL5(1, true)))
val modelGpsL1L5 = SignalInfoViewModel(context, context.applicationContext as Application, repository , PreferenceManager.getDefaultSharedPreferences(context))
modelGpsL1L5.updateStatus(context, listOf(gpsL1(1, false), gpsL5(1, true)), PreferenceManager.getDefaultSharedPreferences(context))
assertEquals(1, modelGpsL1L5.filteredGnssSatellites.value?.size)
assertEquals(1, modelGpsL1L5.getSupportedGnss().size)
assertEquals(0, modelGpsL1L5.getSupportedSbas().size)
@@ -143,7 +147,7 @@ class SignalInfoViewModelTest {
modelGpsL1L5.reset();
// Test GPS L1 + L5 same sv - should be 1 satellite, dual-frequency in view and use
modelGpsL1L5.updateStatus(listOf(gpsL1(1, true), gpsL5(1, true)))
modelGpsL1L5.updateStatus(context, listOf(gpsL1(1, true), gpsL5(1, true)), PreferenceManager.getDefaultSharedPreferences(context))
assertEquals(1, modelGpsL1L5.filteredGnssSatellites.value?.size)
assertEquals(1, modelGpsL1L5.getSupportedGnss().size)
assertEquals(0, modelGpsL1L5.getSupportedSbas().size)
@@ -176,7 +180,7 @@ class SignalInfoViewModelTest {
modelGpsL1L5.reset();
// Test GPS L1 + L5 same sv - should be 1 satellite, dual-frequency in view and but not used (only 1 sv in use)
modelGpsL1L5.updateStatus(listOf(gpsL1(1, true), gpsL5(1, false)))
modelGpsL1L5.updateStatus(context, listOf(gpsL1(1, true), gpsL5(1, false)), PreferenceManager.getDefaultSharedPreferences(context))
assertEquals(1, modelGpsL1L5.filteredGnssSatellites.value?.size)
assertEquals(1, modelGpsL1L5.getSupportedGnss().size)
assertEquals(0, modelGpsL1L5.getSupportedSbas().size)
@@ -209,7 +213,7 @@ class SignalInfoViewModelTest {
modelGpsL1L5.reset();
// Test GPS L1 + L5 but different satellites - should be 2 satellites, non-primary frequency in view and in use, but not dual-frequency in view or use
modelGpsL1L5.updateStatus(listOf(gpsL1(1, true), gpsL5(2, true)))
modelGpsL1L5.updateStatus(context, listOf(gpsL1(1, true), gpsL5(2, true)), PreferenceManager.getDefaultSharedPreferences(context))
assertEquals(2, modelGpsL1L5.filteredGnssSatellites.value?.size)
assertEquals(1, modelGpsL1L5.getSupportedGnss().size)
assertEquals(0, modelGpsL1L5.getSupportedSbas().size)
@@ -246,7 +250,7 @@ class SignalInfoViewModelTest {
modelGpsL1L5.reset();
// Test GPS L1 + L5 same sv, but no L1 signal - should be 1 satellite, dual-frequency not in view or in use
modelGpsL1L5.updateStatus(listOf(gpsL1NoSignal(1), gpsL5(1, true)))
modelGpsL1L5.updateStatus(context, listOf(gpsL1NoSignal(1), gpsL5(1, true)), PreferenceManager.getDefaultSharedPreferences(context))
assertEquals(1, modelGpsL1L5.filteredGnssSatellites.value?.size)
assertEquals(1, modelGpsL1L5.getSupportedGnss().size)
assertEquals(0, modelGpsL1L5.getSupportedSbas().size)
@@ -279,8 +283,8 @@ class SignalInfoViewModelTest {
modelGpsL1L5.reset();
// Test GPS L5 not in use - should be 1 satellites, non-primary frequency in view, but not dual-frequency in view or use
val modelGpsL5 = SignalInfoViewModel(InstrumentationRegistry.getTargetContext().applicationContext as Application, repository)
modelGpsL5.updateStatus(listOf(gpsL5(1, false)))
val modelGpsL5 = SignalInfoViewModel(context, context.applicationContext as Application, repository, PreferenceManager.getDefaultSharedPreferences(context))
modelGpsL5.updateStatus(context, listOf(gpsL5(1, false)), PreferenceManager.getDefaultSharedPreferences(context))
assertEquals(1, modelGpsL5.filteredGnssSatellites.value?.size)
assertEquals(1, modelGpsL5.getSupportedGnss().size)
assertEquals(0, modelGpsL5.getSupportedSbas().size)
@@ -314,8 +318,8 @@ class SignalInfoViewModelTest {
}
// Test GPS L1 + GLONASS L1 - should be 2 satellites, no non-primary carrier of dual-freq
val modelGpsL1GlonassL1 = SignalInfoViewModel(InstrumentationRegistry.getTargetContext().applicationContext as Application, repository)
modelGpsL1GlonassL1.updateStatus(listOf(gpsL1(1, true), glonassL1variant1()))
val modelGpsL1GlonassL1 = SignalInfoViewModel(context, context.applicationContext as Application, repository, PreferenceManager.getDefaultSharedPreferences(context))
modelGpsL1GlonassL1.updateStatus(context, listOf(gpsL1(1, true), glonassL1variant1()), PreferenceManager.getDefaultSharedPreferences(context))
assertEquals(2, modelGpsL1GlonassL1.filteredGnssSatellites.value?.size)
assertFalse(modelGpsL1GlonassL1.isNonPrimaryCarrierFreqInView)
assertFalse(modelGpsL1GlonassL1.isNonPrimaryCarrierFreqInUse)
@@ -340,8 +344,8 @@ class SignalInfoViewModelTest {
}
// Test Galileo E1 + E5a - should be 2 satellites, dual frequency not in use, non-primary carrier of dual-freq
val modelGalileoE1E5a = SignalInfoViewModel(InstrumentationRegistry.getTargetContext().applicationContext as Application, repository)
modelGalileoE1E5a.updateStatus(listOf(galileoE1(1, true), galileoE5a(2, true)))
val modelGalileoE1E5a = SignalInfoViewModel(context, context.applicationContext as Application, repository, PreferenceManager.getDefaultSharedPreferences(context))
modelGalileoE1E5a.updateStatus(context, listOf(galileoE1(1, true), galileoE5a(2, true)), PreferenceManager.getDefaultSharedPreferences(context))
assertEquals(2, modelGalileoE1E5a.filteredGnssSatellites.value?.size)
assertEquals(1, modelGalileoE1E5a.getSupportedGnss().size)
assertEquals(0, modelGalileoE1E5a.getSupportedSbas().size)
@@ -378,7 +382,7 @@ class SignalInfoViewModelTest {
modelGalileoE1E5a.reset()
// Test Galileo E1 + E5a - should be 1 satellites, dual frequency in use, non-primary carrier of dual-freq
modelGalileoE1E5a.updateStatus(listOf(galileoE1(1, true), galileoE5a(1, true)))
modelGalileoE1E5a.updateStatus(context, listOf(galileoE1(1, true), galileoE5a(1, true)), PreferenceManager.getDefaultSharedPreferences(context))
assertEquals(1, modelGalileoE1E5a.filteredGnssSatellites.value?.size)
assertEquals(1, modelGalileoE1E5a.getSupportedGnss().size)
assertEquals(0, modelGalileoE1E5a.getSupportedSbas().size)
@@ -410,8 +414,8 @@ class SignalInfoViewModelTest {
modelGalileoE1E5a.reset()
// Test WAAS SBAS - L1 - should be 1 satellite, dual frequency not in use, no non-primary carrier of dual-freq
val modelWaasL1L5 = SignalInfoViewModel(InstrumentationRegistry.getTargetContext().applicationContext as Application, repository)
modelWaasL1L5.updateStatus(listOf(galaxy15_135L1(true)))
val modelWaasL1L5 = SignalInfoViewModel(context, InstrumentationRegistry.getTargetContext().applicationContext as Application, repository, PreferenceManager.getDefaultSharedPreferences(context))
modelWaasL1L5.updateStatus(context, listOf(galaxy15_135L1(true)), PreferenceManager.getDefaultSharedPreferences(context))
assertEquals(1, modelWaasL1L5.filteredSbasSatellites.value?.size)
assertFalse(modelWaasL1L5.isNonPrimaryCarrierFreqInView)
assertFalse(modelWaasL1L5.isNonPrimaryCarrierFreqInUse)
@@ -437,7 +441,7 @@ class SignalInfoViewModelTest {
modelWaasL1L5.reset()
// Test WAAS SBAS - L1 + L5 - should be 1 satellites, dual frequency in use, non-primary carrier of dual-freq
modelWaasL1L5.updateStatus(listOf(galaxy15_135L1(true), galaxy15_135L5(true)))
modelWaasL1L5.updateStatus(context, listOf(galaxy15_135L1(true), galaxy15_135L5(true)), PreferenceManager.getDefaultSharedPreferences(context))
assertEquals(1, modelWaasL1L5.filteredSbasSatellites.value?.size)
assertEquals(0, modelWaasL1L5.getSupportedGnss().size)
assertEquals(0, modelWaasL1L5.getSupportedGnssCfs().size)

View File

@@ -15,10 +15,10 @@
*/
package com.android.gpstest
import com.android.gpstest.model.GnssType
import com.android.gpstest.model.SatelliteStatus
import com.android.gpstest.model.SatelliteStatus.Companion.NO_DATA
import com.android.gpstest.model.SbasType
import com.android.gpstest.library.model.GnssType
import com.android.gpstest.library.model.SatelliteStatus
import com.android.gpstest.library.model.SatelliteStatus.Companion.NO_DATA
import com.android.gpstest.library.model.SbasType
/**
* Returns a status for a GPS NAVSTAR L1 signal

View File

@@ -23,8 +23,8 @@ import android.content.res.Resources;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
import com.android.gpstest.model.CoordinateType;
import com.android.gpstest.util.UIUtils;
import com.android.gpstest.library.model.CoordinateType;
import com.android.gpstest.library.util.LibUIUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -39,13 +39,13 @@ public class UIUtilsAndroidTest {
// Test German
setLocale("de", "DE");
String dms = UIUtils.getDMSFromLocation(getTargetContext(), -42.853583, CoordinateType.LATITUDE);
String dms = LibUIUtils.getDMSFromLocation(getTargetContext(), -42.853583, CoordinateType.LATITUDE);
assertEquals("S\t\u200742° 51' 12,90\"", dms);
// Test English
setLocale("en", "US");
dms = UIUtils.getDMSFromLocation(getTargetContext(), -42.853583, CoordinateType.LATITUDE);
dms = LibUIUtils.getDMSFromLocation(getTargetContext(), -42.853583, CoordinateType.LATITUDE);
assertEquals("S\t\u200742° 51' 12.90\"", dms);
}
@@ -54,13 +54,13 @@ public class UIUtilsAndroidTest {
// Test German
setLocale("de", "DE");
String dms = UIUtils.getDMSFromLocation(getTargetContext(), 47.64896, CoordinateType.LONGITUDE);
String dms = LibUIUtils.getDMSFromLocation(getTargetContext(), 47.64896, CoordinateType.LONGITUDE);
assertEquals("E\t047° 38' 56,26\"", dms);
// Test English
setLocale("en", "US");
dms = UIUtils.getDMSFromLocation(getTargetContext(), 47.64896, CoordinateType.LONGITUDE);
dms = LibUIUtils.getDMSFromLocation(getTargetContext(), 47.64896, CoordinateType.LONGITUDE);
assertEquals("E\t047° 38' 56.26\"", dms);
}
@@ -69,13 +69,13 @@ public class UIUtilsAndroidTest {
// Test German
setLocale("de", "DE");
String ddm = UIUtils.getDDMFromLocation(getTargetContext(), 24.15346, CoordinateType.LATITUDE);
String ddm = LibUIUtils.getDDMFromLocation(getTargetContext(), 24.15346, CoordinateType.LATITUDE);
assertEquals("N\t\u200724° 09,208", ddm);
// Test English
setLocale("en", "US");
ddm = UIUtils.getDDMFromLocation(getTargetContext(), 24.15346, CoordinateType.LATITUDE);
ddm = LibUIUtils.getDDMFromLocation(getTargetContext(), 24.15346, CoordinateType.LATITUDE);
assertEquals("N\t\u200724° 09.208", ddm);
}
@@ -84,13 +84,13 @@ public class UIUtilsAndroidTest {
// Test English
setLocale("en", "US");
String ddm = UIUtils.getDDMFromLocation(getTargetContext(), -150.94523, CoordinateType.LONGITUDE);
String ddm = LibUIUtils.getDDMFromLocation(getTargetContext(), -150.94523, CoordinateType.LONGITUDE);
assertEquals("W\t150° 56.714", ddm);
// Test German
setLocale("de", "DE");
ddm = UIUtils.getDDMFromLocation(getTargetContext(), -150.94523, CoordinateType.LONGITUDE);
ddm = LibUIUtils.getDDMFromLocation(getTargetContext(), -150.94523, CoordinateType.LONGITUDE);
assertEquals("W\t150° 56,714", ddm);
}

View File

@@ -32,15 +32,16 @@ import androidx.lifecycle.coroutineScope
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.android.gpstest.Application
import com.android.gpstest.Application.Companion.prefs
import com.android.gpstest.R
import com.android.gpstest.data.LocationRepository
import com.android.gpstest.library.data.LocationRepository
import com.android.gpstest.map.MapConstants
import com.android.gpstest.map.MapViewModelController
import com.android.gpstest.map.MapViewModelController.MapInterface
import com.android.gpstest.map.OnMapClickListener
import com.android.gpstest.util.MapUtils
import com.android.gpstest.util.MathUtils
import com.android.gpstest.util.PreferenceUtil
import com.android.gpstest.library.util.MathUtils
import com.android.gpstest.library.util.PreferenceUtil
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.maps.CameraUpdateFactory
@@ -94,7 +95,7 @@ class MapFragment : SupportMapFragment(), View.OnClickListener, LocationSource,
// Preference listener that will cancel the above flows when the user turns off tracking via UI
private val trackingListener: SharedPreferences.OnSharedPreferenceChangeListener =
PreferenceUtil.newStopTrackingListener { onGnssStopped() }
PreferenceUtil.newStopTrackingListener ({ onGnssStopped() }, prefs)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,

View File

@@ -35,7 +35,8 @@
<activity
android:name=".ui.MainActivity"
android:theme="@style/AppTheme.NoActionBar"
android:launchMode="singleInstance">
android:launchMode="singleInstance"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -50,7 +51,8 @@
</activity>
<activity
android:name=".ui.Preferences"
android:label="@string/pref_title">
android:label="@string/pref_title"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />

View File

@@ -58,7 +58,6 @@ class Application : MultiDexApplication() {
lateinit var localeManager: LocaleManager
private set
lateinit var prefs: SharedPreferences
private set
}

View File

@@ -42,37 +42,39 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.android.gpstest.data.LocationRepository
import com.android.gpstest.Application.Companion.app
import com.android.gpstest.Application.Companion.prefs
import com.android.gpstest.library.data.LocationRepository
import com.android.gpstest.io.CsvFileLogger
import com.android.gpstest.io.JsonFileLogger
import com.android.gpstest.model.SatelliteGroup
import com.android.gpstest.model.SatelliteMetadata
import com.android.gpstest.library.model.SatelliteGroup
import com.android.gpstest.library.model.SatelliteMetadata
import com.android.gpstest.ui.MainActivity
import com.android.gpstest.util.FormatUtils.toNotificationTitle
import com.android.gpstest.util.IOUtils.*
import com.android.gpstest.util.PreferenceUtil
import com.android.gpstest.util.PreferenceUtil.injectPsdsWhenLogging
import com.android.gpstest.util.PreferenceUtil.injectTimeWhenLogging
import com.android.gpstest.util.PreferenceUtil.isCsvLoggingEnabled
import com.android.gpstest.util.PreferenceUtil.isJsonLoggingEnabled
import com.android.gpstest.util.PreferenceUtil.writeAntennaInfoToFileCsv
import com.android.gpstest.util.PreferenceUtil.writeAntennaInfoToFileJson
import com.android.gpstest.util.PreferenceUtil.writeLocationToFile
import com.android.gpstest.util.PreferenceUtil.writeMeasurementToLogcat
import com.android.gpstest.util.PreferenceUtil.writeMeasurementsToFile
import com.android.gpstest.util.PreferenceUtil.writeNavMessageToFile
import com.android.gpstest.util.PreferenceUtil.writeNavMessageToLogcat
import com.android.gpstest.util.PreferenceUtil.writeNmeaTimestampToLogcat
import com.android.gpstest.util.PreferenceUtil.writeNmeaToAndroidMonitor
import com.android.gpstest.util.PreferenceUtil.writeNmeaToFile
import com.android.gpstest.util.PreferenceUtil.writeOrientationToFile
import com.android.gpstest.util.PreferenceUtil.writeStatusToFile
import com.android.gpstest.util.PreferenceUtils
import com.android.gpstest.util.SatelliteUtil.toSatelliteGroup
import com.android.gpstest.util.SatelliteUtil.toSatelliteStatus
import com.android.gpstest.util.SatelliteUtils
import com.android.gpstest.util.UIUtils.toNotificationSummary
import com.android.gpstest.util.hasPermission
import com.android.gpstest.library.util.FormatUtils.toNotificationTitle
import com.android.gpstest.library.util.IOUtils.*
import com.android.gpstest.library.util.PreferenceUtil
import com.android.gpstest.library.util.PreferenceUtil.injectPsdsWhenLogging
import com.android.gpstest.library.util.PreferenceUtil.injectTimeWhenLogging
import com.android.gpstest.library.util.PreferenceUtil.isCsvLoggingEnabled
import com.android.gpstest.library.util.PreferenceUtil.isJsonLoggingEnabled
import com.android.gpstest.library.util.PreferenceUtil.writeAntennaInfoToFileCsv
import com.android.gpstest.library.util.PreferenceUtil.writeAntennaInfoToFileJson
import com.android.gpstest.library.util.PreferenceUtil.writeLocationToFile
import com.android.gpstest.library.util.PreferenceUtil.writeMeasurementToLogcat
import com.android.gpstest.library.util.PreferenceUtil.writeMeasurementsToFile
import com.android.gpstest.library.util.PreferenceUtil.writeNavMessageToFile
import com.android.gpstest.library.util.PreferenceUtil.writeNavMessageToLogcat
import com.android.gpstest.library.util.PreferenceUtil.writeNmeaTimestampToLogcat
import com.android.gpstest.library.util.PreferenceUtil.writeNmeaToAndroidMonitor
import com.android.gpstest.library.util.PreferenceUtil.writeNmeaToFile
import com.android.gpstest.library.util.PreferenceUtil.writeOrientationToFile
import com.android.gpstest.library.util.PreferenceUtil.writeStatusToFile
import com.android.gpstest.library.util.PreferenceUtils
import com.android.gpstest.library.util.SatelliteUtil.toSatelliteGroup
import com.android.gpstest.library.util.SatelliteUtil.toSatelliteStatus
import com.android.gpstest.library.util.SatelliteUtils
import com.android.gpstest.library.util.LibUIUtils.toNotificationSummary
import com.android.gpstest.library.util.hasPermission
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.launchIn
@@ -105,7 +107,7 @@ class ForegroundOnlyLocationService : LifecycleService() {
// We save a local reference to last location and SatelliteStatus to create a Notification
private var currentLocation: Location? = null
private var currentSatellites: SatelliteGroup = SatelliteGroup(emptyMap(),SatelliteMetadata())
private var currentSatellites: SatelliteGroup = SatelliteGroup(emptyMap(), SatelliteMetadata())
// Repository of location data that the service will observe, injected via Hilt
@Inject
@@ -125,7 +127,7 @@ class ForegroundOnlyLocationService : LifecycleService() {
// Preference listener that will init the loggers if the user changes Settings while Service is running
private val loggingSettingListener: SharedPreferences.OnSharedPreferenceChangeListener =
PreferenceUtil.newFileLoggingListener { initLogging() }
PreferenceUtil.newFileLoggingListener(app, { initLogging() }, prefs)
private var deletedFiles = false
override fun onCreate() {
@@ -157,7 +159,7 @@ class ForegroundOnlyLocationService : LifecycleService() {
try {
observeFlows()
} catch (unlikely: Exception) {
PreferenceUtils.saveTrackingStarted(false)
PreferenceUtils.saveTrackingStarted(false, prefs)
Log.e(TAG, "Exception registering for updates: $unlikely")
}
@@ -230,7 +232,7 @@ class ForegroundOnlyLocationService : LifecycleService() {
fun subscribeToLocationUpdates() {
Log.d(TAG, "subscribeToLocationUpdates()")
PreferenceUtils.saveTrackingStarted(true)
PreferenceUtils.saveTrackingStarted(true, prefs)
// Binding to this service doesn't actually trigger onStartCommand(). That is needed to
// ensure this Service can be promoted to a foreground service, i.e., the service needs to
@@ -246,12 +248,12 @@ class ForegroundOnlyLocationService : LifecycleService() {
stopSelf()
stopLogging()
isStarted = false
PreferenceUtils.saveTrackingStarted(false)
PreferenceUtils.saveTrackingStarted(false, prefs)
removeOngoingActivityNotification()
currentLocation = null
currentSatellites = SatelliteGroup(emptyMap(),SatelliteMetadata())
currentSatellites = SatelliteGroup(emptyMap(), SatelliteMetadata())
} catch (unlikely: SecurityException) {
PreferenceUtils.saveTrackingStarted(true)
PreferenceUtils.saveTrackingStarted(true, prefs)
Log.e(TAG, "Lost location permissions. Couldn't remove updates. $unlikely")
}
}
@@ -300,7 +302,7 @@ class ForegroundOnlyLocationService : LifecycleService() {
)
GlobalScope.launch(Dispatchers.IO) {
if (writeLocationToFile() &&
if (writeLocationToFile(app, prefs) &&
applicationContext.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
) {
initLogging()
@@ -334,7 +336,7 @@ class ForegroundOnlyLocationService : LifecycleService() {
)
// Log Status
GlobalScope.launch(Dispatchers.IO) {
if (writeStatusToFile() &&
if (writeStatusToFile(app, prefs) &&
applicationContext.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
) {
initLogging()
@@ -357,13 +359,13 @@ class ForegroundOnlyLocationService : LifecycleService() {
.onEach {
//Log.d(TAG, "Service NMEA: $it")
GlobalScope.launch(Dispatchers.IO) {
if (writeNmeaToAndroidMonitor()) {
if (writeNmeaToAndroidMonitor(app, prefs)) {
writeNmeaToAndroidStudio(
it.message,
if (writeNmeaTimestampToLogcat()) it.timestamp else Long.MIN_VALUE
if (writeNmeaTimestampToLogcat(app, prefs)) it.timestamp else Long.MIN_VALUE
)
}
if (writeNmeaToFile() &&
if (writeNmeaToFile(app, prefs) &&
applicationContext.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
) {
initLogging()
@@ -386,10 +388,10 @@ class ForegroundOnlyLocationService : LifecycleService() {
.onEach {
//Log.d(TAG, "Service nav message: $it")
GlobalScope.launch(Dispatchers.IO) {
if (writeNavMessageToLogcat()) {
if (writeNavMessageToLogcat(app, prefs)) {
writeNavMessageToAndroidStudio(it)
}
if (writeNavMessageToFile() &&
if (writeNavMessageToFile(app, prefs) &&
applicationContext.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
) {
initLogging()
@@ -412,12 +414,12 @@ class ForegroundOnlyLocationService : LifecycleService() {
.onEach {
//Log.d(TAG, "Service measurement: $it")
GlobalScope.launch(Dispatchers.IO) {
if (writeMeasurementToLogcat()) {
if (writeMeasurementToLogcat(app, prefs)) {
for (m in it.measurements) {
writeMeasurementToLogcat(m)
}
}
if (writeMeasurementsToFile() &&
if (writeMeasurementsToFile(app, prefs) &&
applicationContext.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
) {
initLogging()
@@ -444,13 +446,13 @@ class ForegroundOnlyLocationService : LifecycleService() {
if (!applicationContext.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
return@launch
}
if (writeAntennaInfoToFileCsv() || writeAntennaInfoToFileJson()) {
if (writeAntennaInfoToFileCsv(app, prefs) || writeAntennaInfoToFileJson(app, prefs)) {
initLogging()
}
if (writeAntennaInfoToFileCsv()) {
if (writeAntennaInfoToFileCsv(app, prefs)) {
csvFileLogger.onGnssAntennaInfoReceived(it)
}
if (writeAntennaInfoToFileJson()) {
if (writeAntennaInfoToFileJson(app, prefs)) {
jsonFileLogger.onGnssAntennaInfoReceived(it)
}
}
@@ -469,7 +471,7 @@ class ForegroundOnlyLocationService : LifecycleService() {
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.onEach {
//Log.d(TAG, "Service sensor: orientation ${it.values[0]}, tilt ${it.values[1]}")
if (writeOrientationToFile() &&
if (writeOrientationToFile(app, prefs) &&
applicationContext.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
) {
initLogging()
@@ -482,7 +484,7 @@ class ForegroundOnlyLocationService : LifecycleService() {
private fun goForegroundOrStopSelf() {
lifecycleScope.launch {
// We may have been restarted by the system - check if we're still monitoring data
if (PreferenceUtils.isTrackingStarted()) {
if (PreferenceUtils.isTrackingStarted(prefs)) {
// Monitoring GNSS data
postOngoingActivityNotification()
} else {
@@ -522,8 +524,8 @@ class ForegroundOnlyLocationService : LifecycleService() {
* Generates a BIG_TEXT_STYLE Notification that represent latest location.
*/
private fun buildNotification(location: Location?, satellites: SatelliteGroup): Notification {
val titleText = satellites.toNotificationTitle()
val summaryText = location?.toNotificationSummary() ?: getString(R.string.no_location_text)
val titleText = satellites.toNotificationTitle(app)
val summaryText = location?.toNotificationSummary(app, prefs) ?: getString(R.string.no_location_text)
// 2. Build the BIG_TEXT_STYLE.
val bigTextStyle = NotificationCompat.BigTextStyle()
@@ -606,20 +608,20 @@ class ForegroundOnlyLocationService : LifecycleService() {
val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
// Inject time and/or PSDS to make sure timestamps and assistance are as updated as possible
if (injectTimeWhenLogging()) {
forceTimeInjection(locationManager)
if (injectTimeWhenLogging(app, prefs)) {
forceTimeInjection(app, locationManager)
}
if (injectPsdsWhenLogging()) {
forcePsdsInjection(locationManager)
if (injectPsdsWhenLogging(app, prefs)) {
forcePsdsInjection(app, locationManager)
}
val date = Date()
if (!csvFileLogger.isStarted && isCsvLoggingEnabled()) {
if (!csvFileLogger.isStarted && isCsvLoggingEnabled(app, prefs)) {
// User has granted permissions and has chosen to log at least one data type
csvFileLogger.startLog(null, date)
}
if (!jsonFileLogger.isStarted && isJsonLoggingEnabled()) {
if (!jsonFileLogger.isStarted && isJsonLoggingEnabled(app, prefs)) {
jsonFileLogger.startLog(null, date)
}
maybeDeleteFiles()

View File

@@ -5,11 +5,12 @@ import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
import com.github.mikephil.charting.formatter.IValueFormatter;
import com.github.mikephil.charting.formatter.ValueFormatter;
import com.github.mikephil.charting.utils.ViewPortHandler;
import java.text.DecimalFormat;
public class DistanceValueFormatter implements IValueFormatter, IAxisValueFormatter {
public class DistanceValueFormatter extends ValueFormatter implements IValueFormatter, IAxisValueFormatter {
private final DecimalFormat mFormat;
private String mSuffix;

View File

@@ -1,69 +0,0 @@
package com.android.gpstest.di
import android.content.Context
import com.android.gpstest.data.*
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.GlobalScope
import javax.inject.Singleton
/**
* Configuration for DI on the repository and shared location manager
*/
@Module
@InstallIn(SingletonComponent::class)
object DataModule {
@Provides
@Singleton
fun provideSharedLocationManager(
@ApplicationContext context: Context
): SharedLocationManager =
SharedLocationManager(context, GlobalScope)
@Provides
@Singleton
fun provideSharedGnssStatusManager(
@ApplicationContext context: Context
): SharedGnssStatusManager =
SharedGnssStatusManager(context, GlobalScope)
@Provides
@Singleton
fun provideSharedNmeaManager(
@ApplicationContext context: Context
): SharedNmeaManager =
SharedNmeaManager(context, GlobalScope)
@Provides
@Singleton
fun provideSharedSensorManager(
@ApplicationContext context: Context
): SharedSensorManager =
SharedSensorManager(context, GlobalScope)
@Provides
@Singleton
fun provideSharedNavMessageManager(
@ApplicationContext context: Context
): SharedNavMessageManager =
SharedNavMessageManager(context, GlobalScope)
@Provides
@Singleton
fun provideSharedMeasurementsManager(
@ApplicationContext context: Context
): SharedGnssMeasurementManager =
SharedGnssMeasurementManager(context, GlobalScope)
@Provides
@Singleton
fun provideSharedAntennaManager(
@ApplicationContext context: Context
): SharedAntennaManager =
SharedAntennaManager(context, GlobalScope)
}

View File

@@ -37,10 +37,10 @@ import androidx.core.content.ContextCompat;
import com.android.gpstest.Application;
import com.android.gpstest.BuildConfig;
import com.android.gpstest.R;
import com.android.gpstest.model.Orientation;
import com.android.gpstest.model.SatelliteStatus;
import com.android.gpstest.util.FormatUtils;
import com.android.gpstest.util.IOUtils;
import com.android.gpstest.library.model.Orientation;
import com.android.gpstest.library.model.SatelliteStatus;
import com.android.gpstest.library.util.FormatUtils;
import com.android.gpstest.library.util.IOUtils;
import java.io.BufferedWriter;
import java.io.IOException;
@@ -121,7 +121,7 @@ public class CsvFileLogger extends BaseFileLogger implements FileLogger {
version.append("Manufacturer: " + manufacturer + ", ");
version.append("Model: " + model + ", ");
version.append("GNSS HW Year: " + IOUtils.getGnssHardwareYear() + ", ");
version.append("GNSS HW Year: " + IOUtils.getGnssHardwareYear(Application.Companion.getApp()) + ", ");
String versionRelease = Build.VERSION.RELEASE;
version.append("Platform: " + versionRelease + ", ");

View File

@@ -17,7 +17,7 @@ import android.preference.PreferenceManager;
import com.android.gpstest.Application;
import com.android.gpstest.R;
import com.android.gpstest.util.LocaleUtils;
import com.android.gpstest.library.util.LocaleUtils;
import java.util.Locale;

View File

@@ -15,7 +15,7 @@ import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;
import com.android.gpstest.model.MeasuredError;
import com.android.gpstest.library.model.MeasuredError;
import com.android.gpstest.ui.BenchmarkViewModel;
import java.lang.ref.WeakReference;
@@ -32,8 +32,6 @@ public class MapViewModelController {
/**
* Draws a path line on the map between the two points if the distance between the two points
* exceeds a threshold
* @param loc1
* @param loc2
* @return true if the line was drawn, or false if the distance between the points didn't
* exceed the threshold and the line was not drawn
*/

View File

@@ -46,11 +46,12 @@ import androidx.lifecycle.ViewModelProviders;
import com.android.gpstest.Application;
import com.android.gpstest.R;
import com.android.gpstest.chart.DistanceValueFormatter;
import com.android.gpstest.model.AvgError;
import com.android.gpstest.model.MeasuredError;
import com.android.gpstest.util.IOUtils;
import com.android.gpstest.util.MathUtils;
import com.android.gpstest.util.PreferenceUtils;
import com.android.gpstest.library.model.AvgError;
import com.android.gpstest.library.model.MeasuredError;
import com.android.gpstest.library.util.IOUtils;
import com.android.gpstest.library.util.MathUtils;
import com.android.gpstest.library.util.PreferenceUtils;
import com.android.gpstest.library.util.LibUIUtils;
import com.android.gpstest.util.UIUtils;
import com.github.mikephil.charting.charts.LineChart;
import com.github.mikephil.charting.components.Legend;
@@ -121,16 +122,16 @@ public class BenchmarkControllerImpl implements BenchmarkController {
// Set default text size and align units properly
mErrorView.setTextSize(TypedValue.COMPLEX_UNIT_PX, Application.Companion.getApp().getResources().getDimension(R.dimen.ground_truth_sliding_header_vert_text_size));
mAvgErrorView.setTextSize(TypedValue.COMPLEX_UNIT_PX, Application.Companion.getApp().getResources().getDimension(R.dimen.ground_truth_sliding_header_vert_text_size));
UIUtils.setVerticalBias(mErrorUnit, UNIT_VERT_BIAS_INCL_VERT_ERROR);
UIUtils.setVerticalBias(mAvgErrorUnit, UNIT_VERT_BIAS_INCL_VERT_ERROR);
LibUIUtils.setVerticalBias(mErrorUnit, UNIT_VERT_BIAS_INCL_VERT_ERROR);
LibUIUtils.setVerticalBias(mAvgErrorUnit, UNIT_VERT_BIAS_INCL_VERT_ERROR);
} else {
// No altitude provided - Hide vertical error chart card
mVerticalErrorCardView.setVisibility(GONE);
// Set default text size and align units properly
mErrorView.setTextSize(TypedValue.COMPLEX_UNIT_PX, Application.Companion.getApp().getResources().getDimension(R.dimen.ground_truth_sliding_header_error_text_size));
mAvgErrorView.setTextSize(TypedValue.COMPLEX_UNIT_PX, Application.Companion.getApp().getResources().getDimension(R.dimen.ground_truth_sliding_header_error_text_size));
UIUtils.setVerticalBias(mErrorUnit, UNIT_VERT_BIAS_HOR_ERROR_ONLY);
UIUtils.setVerticalBias(mAvgErrorUnit, UNIT_VERT_BIAS_HOR_ERROR_ONLY);
LibUIUtils.setVerticalBias(mErrorUnit, UNIT_VERT_BIAS_HOR_ERROR_ONLY);
LibUIUtils.setVerticalBias(mAvgErrorUnit, UNIT_VERT_BIAS_HOR_ERROR_ONLY);
}
// Collapse card - we have to set height on card manually because card doesn't auto-collapse right when views are within card container
@@ -180,7 +181,7 @@ public class BenchmarkControllerImpl implements BenchmarkController {
mErrorUnit.setText(Application.Companion.getApp().getString(R.string.meters_abbreviation));
} else {
// Feet
mErrorView.setText(Application.Companion.getApp().getString(R.string.benchmark_error, UIUtils.toFeet(error.getError())));
mErrorView.setText(Application.Companion.getApp().getString(R.string.benchmark_error, LibUIUtils.toFeet(error.getError())));
mErrorUnit.setText(Application.Companion.getApp().getString(R.string.feet_abbreviation));
}
}
@@ -194,7 +195,7 @@ public class BenchmarkControllerImpl implements BenchmarkController {
mVertErrorView.setText(Application.Companion.getApp().getString(R.string.benchmark_error, Math.abs(error.getVertError())));
} else {
// Feet
mVertErrorView.setText(Application.Companion.getApp().getString(R.string.benchmark_error, UIUtils.toFeet(Math.abs(error.getVertError()))));
mVertErrorView.setText(Application.Companion.getApp().getString(R.string.benchmark_error, LibUIUtils.toFeet(Math.abs(error.getVertError()))));
}
mVerticalErrorCardView.setVisibility(VISIBLE);
} else {
@@ -220,7 +221,7 @@ public class BenchmarkControllerImpl implements BenchmarkController {
mAvgErrorUnit.setText(Application.Companion.getApp().getString(R.string.meters_abbreviation));
} else {
// Feet
mAvgErrorView.setText(Application.Companion.getApp().getString(R.string.benchmark_error, UIUtils.toFeet(avgError.getAvgError())));
mAvgErrorView.setText(Application.Companion.getApp().getString(R.string.benchmark_error, LibUIUtils.toFeet(avgError.getAvgError())));
mAvgErrorUnit.setText(Application.Companion.getApp().getString(R.string.feet_abbreviation));
}
mAvgErrorLabel.setText(Application.Companion.getApp().getString(R.string.avg_error_label, avgError.getCount()));
@@ -231,7 +232,7 @@ public class BenchmarkControllerImpl implements BenchmarkController {
if (mPrefDistanceUnits.equalsIgnoreCase(METERS)) {
mAvgVertErrorView.setText(Application.Companion.getApp().getString(R.string.benchmark_error, avgError.getAvgVertAbsError()));
} else {
mAvgVertErrorView.setText(Application.Companion.getApp().getString(R.string.benchmark_error, UIUtils.toFeet(avgError.getAvgVertAbsError())));
mAvgVertErrorView.setText(Application.Companion.getApp().getString(R.string.benchmark_error, LibUIUtils.toFeet(avgError.getAvgVertAbsError())));
}
} else {
// Hide any vertical error indication
@@ -337,9 +338,9 @@ public class BenchmarkControllerImpl implements BenchmarkController {
} else {
Location groundTruth;
// If a SHOW_RADAR or geo: URI was passed via an Intent (e.g., from BenchMap or OsmAnd app), use that as ground truth
if (IOUtils.isShowRadarIntent(activity.getIntent()) || IOUtils.isGeoIntent(activity.getIntent())) {
groundTruth = IOUtils.getLocationFromIntent(activity.getIntent());
if (IOUtils.isGeoIntent(activity.getIntent())) {
if (IOUtils.isShowRadarIntent(Application.Companion.getApp(), activity.getIntent()) || IOUtils.isGeoIntent(Application.Companion.getApp(), activity.getIntent())) {
groundTruth = IOUtils.getLocationFromIntent(Application.Companion.getApp(), activity.getIntent());
if (IOUtils.isGeoIntent(Application.Companion.getApp(), activity.getIntent())) {
groundTruth.removeAltitude(); // TODO - RFC 5870 requires altitude height above geoid, which we can't support yet (see #296 and #530), so remove altitude here
}
if (groundTruth != null) {
@@ -351,10 +352,10 @@ public class BenchmarkControllerImpl implements BenchmarkController {
} else if (Application.Companion.getPrefs().contains(GROUND_TRUTH_LAT)) {
// If there is a saved ground truth value from previous executions, start test using that
groundTruth = new Location("ground_truth");
groundTruth.setLatitude(PreferenceUtils.getDouble(GROUND_TRUTH_LAT, Double.NaN));
groundTruth.setLongitude(PreferenceUtils.getDouble(GROUND_TRUTH_LONG, Double.NaN));
groundTruth.setLatitude(PreferenceUtils.getDouble(GROUND_TRUTH_LAT, Double.NaN, Application.Companion.getPrefs()));
groundTruth.setLongitude(PreferenceUtils.getDouble(GROUND_TRUTH_LONG, Double.NaN, Application.Companion.getPrefs()));
if (Application.Companion.getPrefs().contains(GROUND_TRUTH_ALT)) {
groundTruth.setAltitude(PreferenceUtils.getDouble(GROUND_TRUTH_ALT, Double.NaN));
groundTruth.setAltitude(PreferenceUtils.getDouble(GROUND_TRUTH_ALT, Double.NaN, Application.Companion.getPrefs()));
}
restoreGroundTruth(groundTruth);
}
@@ -418,12 +419,12 @@ public class BenchmarkControllerImpl implements BenchmarkController {
mViewModel.setBenchmarkCardCollapsed(true);
mViewModel.setAllowGroundTruthEdit(false);
PreferenceUtils.saveDouble(GROUND_TRUTH_LAT, groundTruthLocation.getLatitude());
PreferenceUtils.saveDouble(GROUND_TRUTH_LONG, groundTruthLocation.getLongitude());
PreferenceUtils.saveDouble(GROUND_TRUTH_LAT, groundTruthLocation.getLatitude(), Application.Companion.getPrefs());
PreferenceUtils.saveDouble(GROUND_TRUTH_LONG, groundTruthLocation.getLongitude(), Application.Companion.getPrefs());
if (groundTruthLocation.hasAltitude()) {
PreferenceUtils.saveDouble(GROUND_TRUTH_ALT, groundTruthLocation.getAltitude());
PreferenceUtils.saveDouble(GROUND_TRUTH_ALT, groundTruthLocation.getAltitude(), Application.Companion.getPrefs());
} else {
PreferenceUtils.remove(GROUND_TRUTH_ALT);
PreferenceUtils.remove(GROUND_TRUTH_ALT, Application.Companion.getPrefs());
}
}
@@ -595,8 +596,8 @@ public class BenchmarkControllerImpl implements BenchmarkController {
horAccuracy = location.getAccuracy();
} else {
// Feet
horError = (float) UIUtils.toFeet(error.getError());
horAccuracy = (float) UIUtils.toFeet(location.getAccuracy());
horError = (float) LibUIUtils.toFeet(error.getError());
horAccuracy = (float) LibUIUtils.toFeet(location.getAccuracy());
}
addErrorToGraph(index, mErrorChart, horError, horAccuracy);
@@ -607,14 +608,14 @@ public class BenchmarkControllerImpl implements BenchmarkController {
vertError = Math.abs(error.getVertError());
} else {
// Feet
vertError = UIUtils.toFeet(Math.abs(error.getVertError()));
vertError = LibUIUtils.toFeet(Math.abs(error.getVertError()));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (mPrefDistanceUnits.equalsIgnoreCase(METERS)) {
vertAccuracy = location.getVerticalAccuracyMeters();
} else {
// Feet
vertAccuracy = (float) UIUtils.toFeet(location.getVerticalAccuracyMeters());
vertAccuracy = (float) LibUIUtils.toFeet(location.getVerticalAccuracyMeters());
}
}

View File

@@ -24,9 +24,9 @@ import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.android.gpstest.model.AvgError;
import com.android.gpstest.model.MeasuredError;
import com.android.gpstest.util.BenchmarkUtils;
import com.android.gpstest.library.model.AvgError;
import com.android.gpstest.library.model.MeasuredError;
import com.android.gpstest.library.util.BenchmarkUtils;
import java.util.ArrayList;
import java.util.List;

View File

@@ -7,9 +7,10 @@ import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import com.android.gpstest.Application;
import com.android.gpstest.R;
import com.android.gpstest.model.GnssType;
import com.android.gpstest.util.PreferenceUtils;
import com.android.gpstest.library.model.GnssType;
import com.android.gpstest.library.util.PreferenceUtils;
import java.util.LinkedHashSet;
import java.util.Set;
@@ -56,7 +57,7 @@ public class GnssFilterDialog extends DialogFragment
}
}
PreferenceUtils.saveGnssFilter(filter);
PreferenceUtils.saveGnssFilter(Application.Companion.getApp(), filter, Application.Companion.getPrefs());
dialog.dismiss();
}

View File

@@ -27,7 +27,7 @@ import androidx.appcompat.app.AppCompatActivity;
import com.android.gpstest.Application;
import com.android.gpstest.BuildConfig;
import com.android.gpstest.R;
import com.android.gpstest.util.IOUtils;
import com.android.gpstest.library.util.IOUtils;
public class HelpActivity extends AppCompatActivity {
@@ -60,8 +60,8 @@ public class HelpActivity extends AppCompatActivity {
.append(versionCode)
.append("-" + BuildConfig.FLAVOR + ")\n");
version.append("GNSS HW Year: " + IOUtils.getGnssHardwareYear() + "\n");
version.append("GNSS HW Name: " + IOUtils.getGnssHardwareModelName() + "\n");
version.append("GNSS HW Year: " + IOUtils.getGnssHardwareYear(Application.Companion.getApp()) + "\n");
version.append("GNSS HW Name: " + IOUtils.getGnssHardwareModelName(Application.Companion.getApp()) + "\n");
String versionRelease = Build.VERSION.RELEASE;
version.append("Platform: " + versionRelease + "\n");

View File

@@ -49,23 +49,28 @@ import androidx.lifecycle.Observer
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.android.gpstest.Application
import com.android.gpstest.Application.Companion.app
import com.android.gpstest.Application.Companion.prefs
import com.android.gpstest.ForegroundOnlyLocationService
import com.android.gpstest.ForegroundOnlyLocationService.LocalBinder
import com.android.gpstest.R
import com.android.gpstest.data.FixState
import com.android.gpstest.data.LocationRepository
import com.android.gpstest.library.data.FixState
import com.android.gpstest.library.data.LocationRepository
import com.android.gpstest.databinding.ActivityMainBinding
import com.android.gpstest.library.ui.SignalInfoViewModel
import com.android.gpstest.library.util.*
import com.android.gpstest.map.MapConstants
import com.android.gpstest.ui.NavigationDrawerFragment.NavigationDrawerCallbacks
import com.android.gpstest.ui.sky.SkyFragment
import com.android.gpstest.ui.status.StatusFragment
import com.android.gpstest.util.*
import com.android.gpstest.util.PreferenceUtil.darkTheme
import com.android.gpstest.util.PreferenceUtil.isFileLoggingEnabled
import com.android.gpstest.util.PreferenceUtil.minDistance
import com.android.gpstest.util.PreferenceUtil.minTimeMillis
import com.android.gpstest.util.PreferenceUtil.runInBackground
import com.android.gpstest.util.PreferenceUtils.isTrackingStarted
import com.android.gpstest.library.util.PreferenceUtil.darkTheme
import com.android.gpstest.library.util.PreferenceUtil.isFileLoggingEnabled
import com.android.gpstest.library.util.PreferenceUtil.minDistance
import com.android.gpstest.library.util.PreferenceUtil.minTimeMillis
import com.android.gpstest.library.util.PreferenceUtil.runInBackground
import com.android.gpstest.library.util.PreferenceUtils.isTrackingStarted
import com.android.gpstest.util.BuildUtils
import com.android.gpstest.util.UIUtils
import com.google.android.material.switchmaterial.SwitchMaterial
import com.google.zxing.integration.android.IntentIntegrator
import dagger.hilt.android.AndroidEntryPoint
@@ -144,18 +149,18 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
// Preference listener that will cancel the above flows when the user turns off tracking via service notification
private val stopTrackingListener: SharedPreferences.OnSharedPreferenceChangeListener =
PreferenceUtil.newStopTrackingListener { gpsStop() }
PreferenceUtil.newStopTrackingListener ({ gpsStop() }, prefs)
/** Called when the activity is first created. */
public override fun onCreate(savedInstanceState: Bundle?) {
// Set theme
if (darkTheme()) {
if (darkTheme(app,Application.prefs)) {
setTheme(R.style.AppTheme_Dark_NoActionBar)
useDarkTheme = true
}
super.onCreate(savedInstanceState)
// Reset the activity title to make sure dynamic locale changes are shown
UIUtils.resetActivityTitle(this)
LibUIUtils.resetActivityTitle(this)
saveInstanceState(savedInstanceState)
// Observe stopping location updates from the service
@@ -163,9 +168,9 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
// Set the default values from the XML file if this is the first execution of the app
PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
initialLanguage = PreferenceUtils.getString(getString(R.string.pref_key_language))
initialMinTimeMillis = minTimeMillis()
initialMinDistance = minDistance()
initialLanguage = PreferenceUtils.getString(getString(R.string.pref_key_language), prefs)
initialMinTimeMillis = minTimeMillis(app, prefs)
initialMinDistance = minDistance(app, prefs)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
@@ -189,7 +194,7 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
// If another app is passing in a ground truth location, recreate the activity to initialize an existing instance
if (IOUtils.isShowRadarIntent(intent) || IOUtils.isGeoIntent(intent)) {
if (IOUtils.isShowRadarIntent(app, intent) || IOUtils.isGeoIntent(app, intent)) {
recreateApp(intent)
}
}
@@ -228,7 +233,7 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
} else {
// Explain permission to user (don't request permission here directly to avoid infinite
// loop if user selects "Don't ask again") in system permission prompt
UIUtils.showLocationPermissionDialog(this)
LibUIUtils.showLocationPermissionDialog(this)
}
maybeRecreateApp()
benchmarkController!!.onResume()
@@ -236,7 +241,7 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == UIUtils.PICKFILE_REQUEST_CODE && resultCode == RESULT_OK) {
if (requestCode == LibUIUtils.PICKFILE_REQUEST_CODE && resultCode == RESULT_OK) {
// User picked a file to share from the Share dialog - update the dialog
val uri = data?.data
if (uri != null) {
@@ -244,7 +249,7 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
val location = lastLocation
shareDialogOpen = true
UIUtils.showShareFragmentDialog(
this, location, isFileLoggingEnabled(),
this, location, isFileLoggingEnabled(app, prefs),
service!!.csvFileLogger, service!!.jsonFileLogger, uri
)
}
@@ -253,11 +258,11 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
val scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
if (scanResult != null) {
val geoUri = scanResult.contents
val l = IOUtils.getLocationFromGeoUri(geoUri)
val l = IOUtils.getLocationFromGeoUri(app, geoUri)
if (l != null) {
l.removeAltitude() // TODO - RFC 5870 requires altitude height above geoid, which we can't support yet (see #296 and #530), so remove altitude here
// Create a SHOW_RADAR intent out of the Geo URI and pass that to set ground truth
val showRadar = IOUtils.createShowRadarIntent(l)
val showRadar = IOUtils.createShowRadarIntent(app, l)
recreateApp(showRadar)
} else {
Toast.makeText(
@@ -272,16 +277,16 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
private fun maybeRecreateApp() {
// If the set language has changed since we created the Activity (e.g., returning from Settings), recreate App
if (Application.prefs.contains(getString(R.string.pref_key_language))) {
val currentLanguage = PreferenceUtils.getString(getString(R.string.pref_key_language))
val currentLanguage = PreferenceUtils.getString(getString(R.string.pref_key_language), prefs)
if (currentLanguage != initialLanguage) {
initialLanguage = currentLanguage
recreateApp(null)
}
}
// If the user changed the location update settings, recreate the App
if (minTimeMillis() != initialMinTimeMillis || minDistance() != initialMinDistance) {
initialMinTimeMillis = minTimeMillis()
initialMinDistance = minDistance()
if (minTimeMillis(app, prefs) != initialMinTimeMillis || minDistance(app, prefs) != initialMinDistance) {
initialMinTimeMillis = minTimeMillis(app, prefs)
initialMinDistance = minDistance(app, prefs)
recreateApp(null)
}
}
@@ -294,17 +299,18 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
*/
private fun recreateApp(currentIntent: Intent?) {
val i = Intent(this, MainActivity::class.java)
if (IOUtils.isShowRadarIntent(currentIntent)) {
if (IOUtils.isShowRadarIntent(app, currentIntent)) {
// If we're creating the app because we got a SHOW_RADAR intent, copy over the intent action and extras
i.action = currentIntent!!.action
i.putExtras(currentIntent.extras!!)
} else if (IOUtils.isGeoIntent(currentIntent)) {
} else if (IOUtils.isGeoIntent(app,currentIntent)) {
// If we're creating the app because we got a geo: intent, turn it into a SHOW_RADAR intent for simplicity (they are used the same way)
val l = IOUtils.getLocationFromGeoUri(
app,
currentIntent!!.data.toString()
)
if (l != null) {
val showRadarIntent = IOUtils.createShowRadarIntent(l)
val showRadarIntent = IOUtils.createShowRadarIntent(app, l)
i.action = showRadarIntent.action
i.putExtras(showRadarIntent.extras!!)
}
@@ -366,24 +372,24 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
).show()
}
if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
UIUtils.promptEnableGps(this)
LibUIUtils.promptEnableGps(app,this)
}
setupStartState(lastSavedInstanceState)
// If the theme has changed (e.g., from Preferences), destroy and recreate to reflect change
val useDarkTheme = darkTheme()
val useDarkTheme = darkTheme(app, prefs)
if (this.useDarkTheme != useDarkTheme) {
this.useDarkTheme = useDarkTheme
recreate()
}
val settings = Application.prefs
checkKeepScreenOn(settings)
UIUtils.autoShowWhatsNew(this)
LibUIUtils.autoShowWhatsNew(prefs, app,this)
}
override fun onPause() {
// Stop GNSS if this isn't a configuration change and the user hasn't opted to run in background
if (!isChangingConfigurations && !runInBackground()) {
if (!isChangingConfigurations && !runInBackground(app, prefs)) {
service?.unsubscribeToLocationUpdates()
}
super.onPause()
@@ -395,7 +401,7 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
if (Application.prefs.getBoolean(
getString(R.string.pref_key_auto_start_gps),
true
) || isTrackingStarted()
) || isTrackingStarted(prefs)
) {
gpsStart()
}
@@ -434,7 +440,7 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
false
)
) {
showDialog(UIUtils.CLEAR_ASSIST_WARNING_DIALOG)
showDialog(LibUIUtils.CLEAR_ASSIST_WARNING_DIALOG)
} else {
deleteAidingData()
}
@@ -445,7 +451,7 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
Preferences::class.java
)
)
NavigationDrawerFragment.NAVDRAWER_ITEM_HELP -> showDialog(UIUtils.HELP_DIALOG)
NavigationDrawerFragment.NAVDRAWER_ITEM_HELP -> showDialog(LibUIUtils.HELP_DIALOG)
NavigationDrawerFragment.NAVDRAWER_ITEM_OPEN_SOURCE -> {
val i = Intent(Intent.ACTION_VIEW)
i.data = Uri.parse(getString(R.string.open_source_github))
@@ -458,7 +464,7 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
if (lastLocation != null) {
locationString = LocationUtils.printLocationDetails(lastLocation)
}
UIUtils.sendEmail(this, email, locationString, signalInfoViewModel)
LibUIUtils.sendEmail(app, email, locationString, signalInfoViewModel, BuildUtils.getPlayServicesVersion(), prefs)
}
}
invalidateOptionsMenu()
@@ -610,7 +616,7 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
private fun forcePsdsInjection() {
val success =
IOUtils.forcePsdsInjection(getSystemService(LOCATION_SERVICE) as LocationManager)
IOUtils.forcePsdsInjection(app, getSystemService(LOCATION_SERVICE) as LocationManager)
if (success) {
Toast.makeText(
this, getString(R.string.force_psds_injection_success),
@@ -618,7 +624,8 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
).show()
PreferenceUtils.saveInt(
Application.app.getString(R.string.capability_key_inject_psds),
PreferenceUtils.CAPABILITY_SUPPORTED
PreferenceUtils.CAPABILITY_SUPPORTED,
prefs
)
} else {
Toast.makeText(
@@ -627,14 +634,15 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
).show()
PreferenceUtils.saveInt(
Application.app.getString(R.string.capability_key_inject_psds),
PreferenceUtils.CAPABILITY_NOT_SUPPORTED
PreferenceUtils.CAPABILITY_NOT_SUPPORTED,
prefs
)
}
}
private fun forceTimeInjection() {
val success =
IOUtils.forceTimeInjection(getSystemService(LOCATION_SERVICE) as LocationManager)
IOUtils.forceTimeInjection(app, getSystemService(LOCATION_SERVICE) as LocationManager)
if (success) {
Toast.makeText(
this, getString(R.string.force_time_injection_success),
@@ -642,7 +650,8 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
).show()
PreferenceUtils.saveInt(
Application.app.getString(R.string.capability_key_inject_time),
PreferenceUtils.CAPABILITY_SUPPORTED
PreferenceUtils.CAPABILITY_SUPPORTED,
prefs
)
} else {
Toast.makeText(
@@ -651,7 +660,8 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
).show()
PreferenceUtils.saveInt(
Application.app.getString(R.string.capability_key_inject_time),
PreferenceUtils.CAPABILITY_NOT_SUPPORTED
PreferenceUtils.CAPABILITY_NOT_SUPPORTED,
prefs
)
}
}
@@ -659,12 +669,12 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
@ExperimentalCoroutinesApi
private fun deleteAidingData() {
// If GPS is currently running, stop it
val lastStartState = isTrackingStarted()
if (isTrackingStarted()) {
val lastStartState = isTrackingStarted(prefs)
if (isTrackingStarted(prefs)) {
gpsStop()
}
val success =
IOUtils.deleteAidingData(getSystemService(LOCATION_SERVICE) as LocationManager)
IOUtils.deleteAidingData(app, getSystemService(LOCATION_SERVICE) as LocationManager)
if (success) {
Toast.makeText(
this, getString(R.string.delete_aiding_data_success),
@@ -672,7 +682,8 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
).show()
PreferenceUtils.saveInt(
Application.app.getString(R.string.capability_key_delete_assist),
PreferenceUtils.CAPABILITY_SUPPORTED
PreferenceUtils.CAPABILITY_SUPPORTED,
prefs
)
} else {
Toast.makeText(
@@ -681,7 +692,8 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
).show()
PreferenceUtils.saveInt(
Application.app.getString(R.string.capability_key_delete_assist),
PreferenceUtils.CAPABILITY_NOT_SUPPORTED
PreferenceUtils.CAPABILITY_NOT_SUPPORTED,
prefs
)
}
// Restart the GPS, if it was previously started, with a slight delay,
@@ -712,7 +724,7 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
@SuppressLint("MissingPermission")
@Synchronized
private fun gpsStart() {
PreferenceUtils.saveTrackingStarted(true)
PreferenceUtils.saveTrackingStarted(true, prefs)
service?.subscribeToLocationUpdates()
showProgressBar()
@@ -721,15 +733,15 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
observeGnssStates()
// Show Toast only if the user has set minTime or minDistance to something other than default values
if (minTimeMillis() != (getString(R.string.pref_gps_min_time_default_sec).toDouble() * SECONDS_TO_MILLISECONDS).toLong() ||
minDistance() != getString(R.string.pref_gps_min_distance_default_meters).toFloat()
if (minTimeMillis(app, prefs) != (getString(R.string.pref_gps_min_time_default_sec).toDouble() * SECONDS_TO_MILLISECONDS).toLong() ||
minDistance(app, prefs) != getString(R.string.pref_gps_min_distance_default_meters).toFloat()
) {
Toast.makeText(
this,
String.format(
getString(R.string.gnss_running),
(minTimeMillis().toDouble() / SECONDS_TO_MILLISECONDS).toString(),
minDistance().toString()
(minTimeMillis(app, prefs).toDouble() / SECONDS_TO_MILLISECONDS).toString(),
minDistance(app, prefs).toString()
),
Toast.LENGTH_SHORT
).show()
@@ -770,7 +782,7 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
val gnssStateObserver = Observer<FixState> { fixState ->
when (fixState) {
is FixState.Acquired -> hideProgressBar()
is FixState.NotAcquired -> if (isTrackingStarted()) showProgressBar()
is FixState.NotAcquired -> if (isTrackingStarted(prefs)) showProgressBar()
}
}
signalInfoViewModel.fixState.observe(
@@ -780,7 +792,7 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
@Synchronized
private fun gpsStop() {
PreferenceUtils.saveTrackingStarted(false)
PreferenceUtils.saveTrackingStarted(false, prefs)
locationFlow?.cancel()
// Reset the options menu to trigger updates to action bar menu items
@@ -791,14 +803,14 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
private fun hideProgressBar() {
val p = progressBar
if (p != null) {
UIUtils.hideViewWithAnimation(p, UIUtils.ANIMATION_DURATION_SHORT_MS)
LibUIUtils.hideViewWithAnimation(p, LibUIUtils.ANIMATION_DURATION_SHORT_MS)
}
}
private fun showProgressBar() {
val p = progressBar
if (p != null) {
UIUtils.showViewWithAnimation(p, UIUtils.ANIMATION_DURATION_SHORT_MS)
LibUIUtils.showViewWithAnimation(p, LibUIUtils.ANIMATION_DURATION_SHORT_MS)
}
}
@@ -821,16 +833,16 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
switch = MenuItemCompat.getActionView(item).findViewById(R.id.gps_switch)
if (switch != null) {
// Initialize state of GPS switch before we set the listener, so we don't double-trigger start or stop
switch!!.isChecked = isTrackingStarted()
switch!!.isChecked = isTrackingStarted(prefs)
// Set up listener for GPS on/off switch
switch!!.setOnClickListener {
// Turn GPS on or off
if (!switch!!.isChecked && isTrackingStarted()) {
if (!switch!!.isChecked && isTrackingStarted(prefs)) {
gpsStop()
service?.unsubscribeToLocationUpdates()
} else {
if (switch!!.isChecked && !isTrackingStarted()) {
if (switch!!.isChecked && !isTrackingStarted(prefs)) {
gpsStart()
}
}
@@ -842,7 +854,7 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
val item = menu.findItem(R.id.share)
if (item != null) {
item.isVisible = lastLocation != null || isFileLoggingEnabled() == true
item.isVisible = lastLocation != null || isFileLoggingEnabled(app, prefs) == true
}
return true
}
@@ -868,16 +880,16 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
val location = lastLocation
shareDialogOpen = true
UIUtils.showShareFragmentDialog(
this, location, isFileLoggingEnabled(),
this, location, isFileLoggingEnabled(app, prefs),
service!!.csvFileLogger, service!!.jsonFileLogger, null
)
}
override fun onCreateDialog(id: Int): Dialog {
when (id) {
UIUtils.WHATSNEW_DIALOG -> return UIUtils.createWhatsNewDialog(this)
UIUtils.HELP_DIALOG -> return UIUtils.createHelpDialog(this)
UIUtils.CLEAR_ASSIST_WARNING_DIALOG -> return createClearAssistWarningDialog()
LibUIUtils.WHATSNEW_DIALOG -> return UIUtils.createWhatsNewDialog(this)
LibUIUtils.HELP_DIALOG -> return UIUtils.createHelpDialog(this)
LibUIUtils.CLEAR_ASSIST_WARNING_DIALOG -> return createClearAssistWarningDialog()
}
return super.onCreateDialog(id)
}
@@ -890,7 +902,8 @@ class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
// Save the preference
PreferenceUtils.saveBoolean(
getString(R.string.pref_key_never_show_clear_assist_warning),
isChecked
isChecked,
prefs
)
}
val icon = ContextCompat.getDrawable(Application.app, R.drawable.ic_delete)

View File

@@ -44,8 +44,8 @@ import androidx.fragment.app.Fragment;
import com.android.gpstest.Application;
import com.android.gpstest.R;
import com.android.gpstest.util.IOUtils;
import com.android.gpstest.util.UIUtils;
import com.android.gpstest.library.util.IOUtils;
import com.android.gpstest.library.util.LibUIUtils;
import com.android.gpstest.view.ScrimInsetsScrollView;
import java.util.ArrayList;
@@ -177,7 +177,7 @@ public class NavigationDrawerFragment extends Fragment {
super.onCreate(savedInstanceState);
SharedPreferences sp = Application.Companion.getPrefs();
if (IOUtils.isShowRadarIntent(getActivity().getIntent())) {
if (IOUtils.isShowRadarIntent(Application.Companion.getApp(), getActivity().getIntent())) {
// If another app (e.g., BenchMap) passed in a ground truth location, show the Accuracy view
mCurrentSelectedPosition = NAVDRAWER_ITEM_ACCURACY;
Log.d(TAG, "Using Accuracy position due to RADAR intent = " + mCurrentSelectedPosition);
@@ -463,7 +463,7 @@ public class NavigationDrawerFragment extends Fragment {
if (isSeparator(itemId)) {
// we are done
UIUtils.setAccessibilityIgnore(view);
LibUIUtils.setAccessibilityIgnore(view);
return view;
}

View File

@@ -33,11 +33,11 @@ import com.android.gpstest.Application.Companion.localeManager
import com.android.gpstest.Application.Companion.prefs
import com.android.gpstest.BuildConfig
import com.android.gpstest.R
import com.android.gpstest.util.PermissionUtils
import com.android.gpstest.util.PreferenceUtil.enableMeasurementsPref
import com.android.gpstest.util.PreferenceUtil.enableNavMessagesPref
import com.android.gpstest.util.SatelliteUtils
import com.android.gpstest.util.UIUtils.resetActivityTitle
import com.android.gpstest.library.util.PermissionUtils
import com.android.gpstest.library.util.PreferenceUtil.enableMeasurementsPref
import com.android.gpstest.library.util.SatelliteUtils
import com.android.gpstest.library.util.LibUIUtils.resetActivityTitle
import com.android.gpstest.library.util.PreferenceUtil.enableNavMessagesPref
class Preferences : PreferenceActivity(), OnSharedPreferenceChangeListener {
var forceFullGnssMeasurements: CheckBoxPreference? = null
@@ -175,7 +175,7 @@ class Preferences : PreferenceActivity(), OnSharedPreferenceChangeListener {
}
chkLogFileNavMessages =
findPreference(getString(R.string.pref_key_file_navigation_message_output)) as CheckBoxPreference
chkLogFileNavMessages?.isEnabled = enableNavMessagesPref()
chkLogFileNavMessages?.isEnabled = enableNavMessagesPref(app, prefs)
chkLogFileNavMessages?.onPreferenceChangeListener =
OnPreferenceChangeListener { _: Preference?, _: Any? ->
PermissionUtils.requestFileWritePermission(this@Preferences)
@@ -183,7 +183,7 @@ class Preferences : PreferenceActivity(), OnSharedPreferenceChangeListener {
}
chkLogFileMeasurements =
findPreference(getString(R.string.pref_key_file_measurement_output)) as CheckBoxPreference
chkLogFileMeasurements?.isEnabled = enableMeasurementsPref();
chkLogFileMeasurements?.isEnabled = enableMeasurementsPref(app, prefs);
chkLogFileMeasurements?.onPreferenceChangeListener =
OnPreferenceChangeListener { preference: Preference?, newValue: Any? ->
PermissionUtils.requestFileWritePermission(this@Preferences)
@@ -223,9 +223,9 @@ class Preferences : PreferenceActivity(), OnSharedPreferenceChangeListener {
// Disable Android Studio logging if not supported by platform
chkAsMeasurements = findPreference(getString(R.string.pref_key_as_measurement_output)) as CheckBoxPreference
chkAsMeasurements?.isEnabled = enableMeasurementsPref()
chkAsMeasurements?.isEnabled = enableMeasurementsPref(app, prefs)
chkAsNavMessages = findPreference(getString(R.string.pref_key_as_navigation_message_output)) as CheckBoxPreference
chkAsNavMessages?.isEnabled = enableNavMessagesPref()
chkAsNavMessages?.isEnabled = enableNavMessagesPref(app, prefs)
prefs.registerOnSharedPreferenceChangeListener(this)
}

View File

@@ -13,11 +13,13 @@ import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.android.gpstest.Application
import com.android.gpstest.Application.Companion.app
import com.android.gpstest.Application.Companion.prefs
import com.android.gpstest.R
import com.android.gpstest.model.CoordinateType
import com.android.gpstest.util.IOUtils
import com.android.gpstest.util.PreferenceUtils
import com.android.gpstest.util.UIUtils
import com.android.gpstest.library.model.CoordinateType
import com.android.gpstest.library.util.IOUtils
import com.android.gpstest.library.util.PreferenceUtils
import com.android.gpstest.library.util.LibUIUtils
import com.google.android.material.button.MaterialButton
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
@@ -64,7 +66,8 @@ class ShareLocationFragment : Fragment() {
// Check selected coordinate format and show in UI
val coordinateFormat = Application.prefs.getString(Application.app.getString(R.string.pref_key_coordinate_format), Application.app.getString(R.string.preferences_coordinate_format_dd_key))
if (location != null) {
UIUtils.formatLocationForDisplay(
LibUIUtils.formatLocationForDisplay(
app,
location,
locationValue,
includeAltitude.isChecked,
@@ -86,7 +89,8 @@ class ShareLocationFragment : Fragment() {
format = "ddm"
}
if (location != null) {
UIUtils.formatLocationForDisplay(
LibUIUtils.formatLocationForDisplay(
app,
location,
locationValue,
isChecked,
@@ -96,7 +100,7 @@ class ShareLocationFragment : Fragment() {
format
)
}
PreferenceUtils.saveBoolean(Application.app.getString(R.string.pref_key_share_include_altitude), isChecked)
PreferenceUtils.saveBoolean(Application.app.getString(R.string.pref_key_share_include_altitude), isChecked, prefs)
}
chipDecimalDegrees.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
@@ -110,8 +114,9 @@ class ShareLocationFragment : Fragment() {
chipDMS.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
if (isChecked) {
if (location != null) {
locationValue.text = IOUtils.createLocationShare(UIUtils.getDMSFromLocation(Application.app, location.latitude, CoordinateType.LATITUDE),
UIUtils.getDMSFromLocation(Application.app, location.longitude, CoordinateType.LONGITUDE),
locationValue.text = IOUtils.createLocationShare(
LibUIUtils.getDMSFromLocation(Application.app, location.latitude, CoordinateType.LATITUDE),
LibUIUtils.getDMSFromLocation(Application.app, location.longitude, CoordinateType.LONGITUDE),
if (location.hasAltitude() && includeAltitude.isChecked) location.altitude.toString() else null)
}
}
@@ -119,8 +124,9 @@ class ShareLocationFragment : Fragment() {
chipDegreesDecimalMin.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
if (isChecked) {
if (location != null) {
locationValue.text = IOUtils.createLocationShare(UIUtils.getDDMFromLocation(Application.app, location.latitude, CoordinateType.LATITUDE),
UIUtils.getDDMFromLocation(Application.app, location.longitude, CoordinateType.LONGITUDE),
locationValue.text = IOUtils.createLocationShare(
LibUIUtils.getDDMFromLocation(Application.app, location.latitude, CoordinateType.LATITUDE),
LibUIUtils.getDDMFromLocation(Application.app, location.longitude, CoordinateType.LONGITUDE),
if (location.hasAltitude() && includeAltitude.isChecked) location.altitude.toString() else null)
}
}
@@ -130,7 +136,7 @@ class ShareLocationFragment : Fragment() {
// Copy to clipboard
if (location != null) {
val locationString = locationValue.text.toString()
IOUtils.copyToClipboard(locationString)
IOUtils.copyToClipboard(app, locationString)
Toast.makeText(activity, R.string.copied_to_clipboard, Toast.LENGTH_LONG).show()
}
}
@@ -149,7 +155,7 @@ class ShareLocationFragment : Fragment() {
// Open the location in another app
if (location != null) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(IOUtils.createGeoUri(location, includeAltitude.isChecked))
intent.data = Uri.parse(IOUtils.createGeoUri(app, location, includeAltitude.isChecked))
if (intent.resolveActivity(requireActivity().packageManager) != null) {
requireActivity().startActivity(intent)
}
@@ -161,7 +167,7 @@ class ShareLocationFragment : Fragment() {
if (location != null) {
val intent = Intent(Intent.ACTION_SEND)
val text: String = if (chipDecimalDegrees.isChecked) {
IOUtils.createGeoUri(location, includeAltitude.isChecked)
IOUtils.createGeoUri(app, location, includeAltitude.isChecked)
} else {
locationValue.text.toString()
}

View File

@@ -8,9 +8,10 @@ import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import com.android.gpstest.Application.Companion.app
import com.android.gpstest.R
import com.android.gpstest.util.IOUtils
import com.android.gpstest.util.UIUtils
import com.android.gpstest.library.util.IOUtils
import com.android.gpstest.library.util.LibUIUtils
import com.google.android.material.button.MaterialButton
import java.io.File
import java.util.*
@@ -88,7 +89,7 @@ class ShareLogFragment : Fragment() {
val uri = IOUtils.getUriFromFile(activity, files?.get(0))
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.data = uri
activity!!.startActivityForResult(intent, UIUtils.PICKFILE_REQUEST_CODE)
activity!!.startActivityForResult(intent, LibUIUtils.PICKFILE_REQUEST_CODE)
// Dismiss the dialog - it will be re-created in the callback to GpsTestActivity
listener.onFileBrowse()
}
@@ -97,11 +98,11 @@ class ShareLogFragment : Fragment() {
// Send the log file
if (alternateFileUri == null && files != null) {
// Send the log file currently being logged to by the FileLogger
IOUtils.sendLogFile(activity, *files.toTypedArray())
IOUtils.sendLogFile(app, activity, *files.toTypedArray())
listener.onLogFileSent()
} else {
// Send the log file selected by the user using the File Browse button
IOUtils.sendLogFile(activity, ArrayList(Collections.singleton(alternateFileUri)))
IOUtils.sendLogFile(app, activity, ArrayList(Collections.singleton(alternateFileUri)))
listener.onLogFileSent()
}
}

View File

@@ -21,13 +21,15 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.coroutineScope
import com.android.gpstest.Application
import com.android.gpstest.Application.Companion.app
import com.android.gpstest.Application.Companion.prefs
import com.android.gpstest.BuildConfig
import com.android.gpstest.R
import com.android.gpstest.io.DevicePropertiesUploader
import com.android.gpstest.ui.SignalInfoViewModel
import com.android.gpstest.util.IOUtils.*
import com.android.gpstest.util.PreferenceUtils
import com.android.gpstest.util.SatelliteUtils
import com.android.gpstest.library.ui.SignalInfoViewModel
import com.android.gpstest.library.util.IOUtils.*
import com.android.gpstest.library.util.PreferenceUtils
import com.android.gpstest.library.util.SatelliteUtils
import com.google.android.material.button.MaterialButton
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
@@ -66,7 +68,7 @@ class UploadDeviceInfoFragment : Fragment() {
uploadNoLocationTextView.visibility = View.GONE
if (Geocoder.isPresent()) {
val geocoder = Geocoder(context)
val geocoder = Geocoder(context!!)
var addresses: List<Address>? = emptyList()
try {
addresses = geocoder.getFromLocation(location.latitude, location.longitude, 1)
@@ -98,10 +100,10 @@ class UploadDeviceInfoFragment : Fragment() {
val psdsSuccessBoolean: Boolean
val psdsSuccessString: String
if (capabilityInjectPsdsInt == PreferenceUtils.CAPABILITY_UNKNOWN) {
psdsSuccessBoolean = forcePsdsInjection(locationManager)
psdsSuccessString = PreferenceUtils.getCapabilityDescription(psdsSuccessBoolean)
psdsSuccessBoolean = forcePsdsInjection(app, locationManager)
psdsSuccessString = PreferenceUtils.getCapabilityDescription(app, psdsSuccessBoolean)
} else {
psdsSuccessString = PreferenceUtils.getCapabilityDescription(capabilityInjectPsdsInt)
psdsSuccessString = PreferenceUtils.getCapabilityDescription(app, capabilityInjectPsdsInt)
}
// Inject time
@@ -109,10 +111,10 @@ class UploadDeviceInfoFragment : Fragment() {
val timeSuccessBoolean: Boolean
val timeSuccessString: String
if (capabilityInjectTimeInt == PreferenceUtils.CAPABILITY_UNKNOWN) {
timeSuccessBoolean = forceTimeInjection(locationManager)
timeSuccessString = PreferenceUtils.getCapabilityDescription(timeSuccessBoolean)
timeSuccessBoolean = forceTimeInjection(app, locationManager)
timeSuccessString = PreferenceUtils.getCapabilityDescription(app, timeSuccessBoolean)
} else {
timeSuccessString = PreferenceUtils.getCapabilityDescription(capabilityInjectTimeInt)
timeSuccessString = PreferenceUtils.getCapabilityDescription(app, capabilityInjectTimeInt)
}
// Delete assist capability
@@ -120,7 +122,7 @@ class UploadDeviceInfoFragment : Fragment() {
val deleteAssistSuccessString: String
if (capabilityDeleteAssistInt != PreferenceUtils.CAPABILITY_UNKNOWN) {
// Deleting assist data can be destructive, so don't force it - just use existing info
deleteAssistSuccessString = PreferenceUtils.getCapabilityDescription(capabilityDeleteAssistInt)
deleteAssistSuccessString = PreferenceUtils.getCapabilityDescription(app, capabilityDeleteAssistInt)
} else {
deleteAssistSuccessString = ""
}
@@ -129,7 +131,7 @@ class UploadDeviceInfoFragment : Fragment() {
val capabilityMeasurementsInt = Application.prefs.getInt(Application.app.getString(R.string.capability_key_raw_measurements), PreferenceUtils.CAPABILITY_UNKNOWN)
val capabilityMeasurementsString: String
if (capabilityMeasurementsInt != PreferenceUtils.CAPABILITY_UNKNOWN) {
capabilityMeasurementsString = PreferenceUtils.getCapabilityDescription(capabilityMeasurementsInt)
capabilityMeasurementsString = PreferenceUtils.getCapabilityDescription(app, capabilityMeasurementsInt)
} else {
capabilityMeasurementsString = ""
}
@@ -138,17 +140,17 @@ class UploadDeviceInfoFragment : Fragment() {
val capabilityNavMessagesInt = Application.prefs.getInt(Application.app.getString(R.string.capability_key_nav_messages), PreferenceUtils.CAPABILITY_UNKNOWN)
val capabilityNavMessagesString: String
if (capabilityNavMessagesInt != PreferenceUtils.CAPABILITY_UNKNOWN) {
capabilityNavMessagesString = PreferenceUtils.getCapabilityDescription(capabilityNavMessagesInt)
capabilityNavMessagesString = PreferenceUtils.getCapabilityDescription(app, capabilityNavMessagesInt)
} else {
capabilityNavMessagesString = ""
}
val gnssAntennaInfo = PreferenceUtils.getCapabilityDescription(SatelliteUtils.isGnssAntennaInfoSupported(locationManager))
val gnssAntennaInfo = PreferenceUtils.getCapabilityDescription(app, SatelliteUtils.isGnssAntennaInfoSupported(locationManager))
val numAntennas: String
val antennaCfs: String
if (gnssAntennaInfo.equals(Application.app.getString(R.string.capability_value_supported))) {
numAntennas = PreferenceUtils.getInt(Application.app.getString(R.string.capability_key_num_antenna), -1).toString()
antennaCfs = PreferenceUtils.getString(Application.app.getString(R.string.capability_key_antenna_cf))
numAntennas = PreferenceUtils.getInt(Application.app.getString(R.string.capability_key_num_antenna), -1, prefs).toString()
antennaCfs = PreferenceUtils.getString(Application.app.getString(R.string.capability_key_antenna_cf), prefs)
} else {
numAntennas = ""
antennaCfs = ""
@@ -161,24 +163,24 @@ class UploadDeviceInfoFragment : Fragment() {
DevicePropertiesUploader.DEVICE to Build.DEVICE,
DevicePropertiesUploader.ANDROID_VERSION to Build.VERSION.RELEASE,
DevicePropertiesUploader.API_LEVEL to Build.VERSION.SDK_INT.toString(),
DevicePropertiesUploader.GNSS_HARDWARE_YEAR to getGnssHardwareYear(),
DevicePropertiesUploader.GNSS_HARDWARE_MODEL_NAME to getGnssHardwareModelName(),
DevicePropertiesUploader.DUAL_FREQUENCY to PreferenceUtils.getCapabilityDescription(signalInfoViewModel.isNonPrimaryCarrierFreqInView),
DevicePropertiesUploader.GNSS_HARDWARE_YEAR to getGnssHardwareYear(app),
DevicePropertiesUploader.GNSS_HARDWARE_MODEL_NAME to getGnssHardwareModelName(app),
DevicePropertiesUploader.DUAL_FREQUENCY to PreferenceUtils.getCapabilityDescription(app, signalInfoViewModel.isNonPrimaryCarrierFreqInView),
DevicePropertiesUploader.SUPPORTED_GNSS to trimEnds(replaceNavstar(signalInfoViewModel.getSupportedGnss().sorted().toString())),
DevicePropertiesUploader.GNSS_CFS to trimEnds(signalInfoViewModel.getSupportedGnssCfs().sorted().toString()),
DevicePropertiesUploader.SUPPORTED_SBAS to trimEnds(signalInfoViewModel.getSupportedSbas().sorted().toString()),
DevicePropertiesUploader.SBAS_CFS to trimEnds(signalInfoViewModel.getSupportedSbasCfs().sorted().toString()),
DevicePropertiesUploader.RAW_MEASUREMENTS to capabilityMeasurementsString,
DevicePropertiesUploader.NAVIGATION_MESSAGES to capabilityNavMessagesString,
DevicePropertiesUploader.NMEA to PreferenceUtils.getCapabilityDescription(Application.prefs.getInt(Application.app.getString(R.string.capability_key_nmea), PreferenceUtils.CAPABILITY_UNKNOWN)),
DevicePropertiesUploader.NMEA to PreferenceUtils.getCapabilityDescription(app, Application.prefs.getInt(Application.app.getString(R.string.capability_key_nmea), PreferenceUtils.CAPABILITY_UNKNOWN)),
DevicePropertiesUploader.INJECT_PSDS to psdsSuccessString,
DevicePropertiesUploader.INJECT_TIME to timeSuccessString,
DevicePropertiesUploader.DELETE_ASSIST to deleteAssistSuccessString,
DevicePropertiesUploader.ACCUMULATED_DELTA_RANGE to PreferenceUtils.getCapabilityDescription(Application.prefs.getInt(Application.app.getString(R.string.capability_key_measurement_delta_range), PreferenceUtils.CAPABILITY_UNKNOWN)),
DevicePropertiesUploader.ACCUMULATED_DELTA_RANGE to PreferenceUtils.getCapabilityDescription(app, Application.prefs.getInt(Application.app.getString(R.string.capability_key_measurement_delta_range), PreferenceUtils.CAPABILITY_UNKNOWN)),
// TODO - Add below clock values? What should they be to generalize across all of the same model?
DevicePropertiesUploader.HARDWARE_CLOCK to "",
DevicePropertiesUploader.HARDWARE_CLOCK_DISCONTINUITY to "",
DevicePropertiesUploader.AUTOMATIC_GAIN_CONTROL to PreferenceUtils.getCapabilityDescription(Application.prefs.getInt(Application.app.getString(R.string.capability_key_measurement_automatic_gain_control), PreferenceUtils.CAPABILITY_UNKNOWN)),
DevicePropertiesUploader.AUTOMATIC_GAIN_CONTROL to PreferenceUtils.getCapabilityDescription(app, Application.prefs.getInt(Application.app.getString(R.string.capability_key_measurement_automatic_gain_control), PreferenceUtils.CAPABILITY_UNKNOWN)),
DevicePropertiesUploader.GNSS_ANTENNA_INFO to gnssAntennaInfo,
DevicePropertiesUploader.APP_BUILD_FLAVOR to BuildConfig.FLAVOR,
DevicePropertiesUploader.USER_COUNTRY to userCountry,
@@ -205,13 +207,13 @@ class UploadDeviceInfoFragment : Fragment() {
lifecycle.coroutineScope.launch {
val uploader = DevicePropertiesUploader(bundle)
if (uploader.upload()) {
Toast.makeText(Application.app, R.string.upload_success, Toast.LENGTH_SHORT).show()
Toast.makeText(app, R.string.upload_success, Toast.LENGTH_SHORT).show()
// Remove app version and code, and then save hash to compare against next upload attempt
bundle.remove(DevicePropertiesUploader.APP_VERSION_NAME)
bundle.remove(DevicePropertiesUploader.APP_VERSION_CODE)
PreferenceUtils.saveInt(Application.app.getString(R.string.capability_key_last_upload_hash), bundle.toString().hashCode())
PreferenceUtils.saveInt(app.getString(R.string.capability_key_last_upload_hash), bundle.toString().hashCode(), prefs)
} else {
Toast.makeText(Application.app, R.string.upload_failure, Toast.LENGTH_SHORT).show()
Toast.makeText(app, R.string.upload_failure, Toast.LENGTH_SHORT).show()
}
upload.isEnabled = true
uploadProgress.visibility = View.INVISIBLE

View File

@@ -41,24 +41,26 @@ import androidx.lifecycle.Observer
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.android.gpstest.Application
import com.android.gpstest.Application.Companion.app
import com.android.gpstest.Application.Companion.prefs
import com.android.gpstest.R
import com.android.gpstest.data.FixState
import com.android.gpstest.data.LocationRepository
import com.android.gpstest.library.data.FixState
import com.android.gpstest.library.data.LocationRepository
import com.android.gpstest.databinding.GpsSkyBinding
import com.android.gpstest.databinding.GpsSkyLegendCardBinding
import com.android.gpstest.databinding.GpsSkySignalMeterBinding
import com.android.gpstest.model.SatelliteMetadata
import com.android.gpstest.model.SatelliteStatus
import com.android.gpstest.ui.SignalInfoViewModel
import com.android.gpstest.library.model.SatelliteMetadata
import com.android.gpstest.library.model.SatelliteStatus
import com.android.gpstest.library.ui.SignalInfoViewModel
import com.android.gpstest.ui.status.Filter
import com.android.gpstest.ui.theme.AppTheme
import com.android.gpstest.util.MathUtils
import com.android.gpstest.util.PreferenceUtil
import com.android.gpstest.util.PreferenceUtil.darkTheme
import com.android.gpstest.util.PreferenceUtils
import com.android.gpstest.util.PreferenceUtils.clearGnssFilter
import com.android.gpstest.util.PreferenceUtils.gnssFilter
import com.android.gpstest.util.UIUtils
import com.android.gpstest.library.util.MathUtils
import com.android.gpstest.library.util.PreferenceUtil
import com.android.gpstest.library.util.PreferenceUtil.darkTheme
import com.android.gpstest.library.util.PreferenceUtils
import com.android.gpstest.library.util.PreferenceUtils.clearGnssFilter
import com.android.gpstest.library.util.PreferenceUtils.gnssFilter
import com.android.gpstest.library.util.LibUIUtils
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
@@ -100,7 +102,7 @@ class SkyFragment : Fragment() {
// Preference listener that will cancel the above flows when the user turns off tracking via UI
private val trackingListener: SharedPreferences.OnSharedPreferenceChangeListener =
PreferenceUtil.newStopTrackingListener { onGnssStopped() }
PreferenceUtil.newStopTrackingListener ({ onGnssStopped() }, prefs)
@OptIn(ExperimentalCoroutinesApi::class)
override fun onCreateView(
@@ -126,7 +128,7 @@ class SkyFragment : Fragment() {
override fun onResume() {
super.onResume()
val color: Int
if (darkTheme()) {
if (darkTheme(app, prefs)) {
// Dark theme
color = ContextCompat.getColor(requireContext(), android.R.color.secondary_text_dark)
legend.skyLegendUsedInFix.setImageResource(R.drawable.circle_used_in_fix_dark)
@@ -182,7 +184,7 @@ class SkyFragment : Fragment() {
.onEach {
when (it) {
is FixState.Acquired -> onGnssFixAcquired()
is FixState.NotAcquired -> if (PreferenceUtils.isTrackingStarted()) onGnssFixLost()
is FixState.NotAcquired -> if (PreferenceUtils.isTrackingStarted(prefs)) onGnssFixLost()
}
}
.launchIn(lifecycleScope)
@@ -252,15 +254,15 @@ class SkyFragment : Fragment() {
// Dispose the Composition when the view's LifecycleOwner is destroyed
setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
setContent {
AppTheme(darkTheme = darkTheme()) {
AppTheme(darkTheme = darkTheme(app, prefs)) {
val allStatuses: List<SatelliteStatus> by viewModel.allStatuses.observeAsState(emptyList())
val satelliteMetadata: SatelliteMetadata by viewModel.filteredSatelliteMetadata.observeAsState(
SatelliteMetadata()
)
// Order of arguments seems to matter in below IF statement - it doesn't seem
// to recompose if gnssFilter().isNotEmpty() is first
if (allStatuses.isNotEmpty() && gnssFilter().isNotEmpty()) {
Filter(allStatuses.size, satelliteMetadata) { clearGnssFilter() }
if (allStatuses.isNotEmpty() && gnssFilter(app, prefs).isNotEmpty()) {
Filter(allStatuses.size, satelliteMetadata) { clearGnssFilter(app, prefs) }
}
}
}
@@ -351,7 +353,7 @@ class SkyFragment : Fragment() {
// Based on the avg C/N0 for "in view" and "used" satellites the left margins need to be adjusted accordingly
val meterWidthPx = (Application.app.resources.getDimension(R.dimen.cn0_meter_width)
.toInt()
- UIUtils.dpToPixels(Application.app, 7.0f)) // Reduce width for padding
- LibUIUtils.dpToPixels(Application.app, 7.0f)) // Reduce width for padding
val minIndicatorMarginPx = Application.app.resources
.getDimension(R.dimen.cn0_indicator_min_left_margin).toInt()
val maxIndicatorMarginPx = meterWidthPx + minIndicatorMarginPx
@@ -365,7 +367,7 @@ class SkyFragment : Fragment() {
// Calculate normal offsets for avg in view satellite C/N0 value TextViews
var leftInViewTextViewMarginPx: Int? = null
if (MathUtils.isValidFloat(binding.skyView.cn0InViewAvg)) {
leftInViewTextViewMarginPx = UIUtils.cn0ToTextViewLeftMarginPx(
leftInViewTextViewMarginPx = LibUIUtils.cn0ToTextViewLeftMarginPx(
binding.skyView.cn0InViewAvg,
minTextViewMarginPx, maxTextViewMarginPx
)
@@ -374,7 +376,7 @@ class SkyFragment : Fragment() {
// Calculate normal offsets for avg used satellite C/N0 value TextViews
var leftUsedTextViewMarginPx: Int? = null
if (MathUtils.isValidFloat(binding.skyView.cn0UsedAvg)) {
leftUsedTextViewMarginPx = UIUtils.cn0ToTextViewLeftMarginPx(
leftUsedTextViewMarginPx = LibUIUtils.cn0ToTextViewLeftMarginPx(
binding.skyView.cn0UsedAvg,
minTextViewMarginPx, maxTextViewMarginPx
)
@@ -382,7 +384,7 @@ class SkyFragment : Fragment() {
// See if we need to apply the offset margin to try and keep the two TextViews from overlapping by shifting one of the two left
if (leftInViewTextViewMarginPx != null && leftUsedTextViewMarginPx != null) {
val offset = UIUtils.dpToPixels(Application.app, TEXTVIEW_NON_OVERLAP_OFFSET_DP)
val offset = LibUIUtils.dpToPixels(Application.app, TEXTVIEW_NON_OVERLAP_OFFSET_DP)
if (leftInViewTextViewMarginPx <= leftUsedTextViewMarginPx) {
leftInViewTextViewMarginPx += offset
} else {
@@ -391,8 +393,8 @@ class SkyFragment : Fragment() {
}
// Define paddings used for TextViews
val pSides = UIUtils.dpToPixels(Application.app, 7f)
val pTopBottom = UIUtils.dpToPixels(Application.app, 4f)
val pSides = LibUIUtils.dpToPixels(Application.app, 7f)
val pTopBottom = LibUIUtils.dpToPixels(Application.app, 4f)
// Set avg C/N0 of satellites in view of device
if (MathUtils.isValidFloat(binding.skyView.cn0InViewAvg)) {
@@ -444,7 +446,7 @@ class SkyFragment : Fragment() {
}
// Set position and visibility of indicator
val leftIndicatorMarginPx = UIUtils.cn0ToIndicatorLeftMarginPx(
val leftIndicatorMarginPx = LibUIUtils.cn0ToIndicatorLeftMarginPx(
binding.skyView.cn0InViewAvg,
minIndicatorMarginPx, maxIndicatorMarginPx
)
@@ -508,7 +510,7 @@ class SkyFragment : Fragment() {
}
// Set position and visibility of indicator
val leftMarginPx = UIUtils.cn0ToIndicatorLeftMarginPx(
val leftMarginPx = LibUIUtils.cn0ToIndicatorLeftMarginPx(
binding.skyView.cn0UsedAvg,
minIndicatorMarginPx, maxIndicatorMarginPx
)
@@ -552,7 +554,7 @@ class SkyFragment : Fragment() {
currentMargin - (abs(currentMargin - goalLeftMarginPx)
* interpolatedTime).toInt()
}
UIUtils.setMargins(
LibUIUtils.setMargins(
v,
newLeft,
p.topMargin,
@@ -569,11 +571,11 @@ class SkyFragment : Fragment() {
}
private fun showHaveFix() {
UIUtils.showViewWithAnimation(binding.skyLock, UIUtils.ANIMATION_DURATION_SHORT_MS)
LibUIUtils.showViewWithAnimation(binding.skyLock, LibUIUtils.ANIMATION_DURATION_SHORT_MS)
}
private fun showLostFix() {
UIUtils.hideViewWithAnimation(binding.skyLock, UIUtils.ANIMATION_DURATION_SHORT_MS)
LibUIUtils.hideViewWithAnimation(binding.skyLock, LibUIUtils.ANIMATION_DURATION_SHORT_MS)
}
companion object {

View File

@@ -49,19 +49,21 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import com.android.gpstest.Application
import com.android.gpstest.Application.Companion.app
import com.android.gpstest.Application.Companion.prefs
import com.android.gpstest.R
import com.android.gpstest.data.FixState
import com.android.gpstest.model.CoordinateType
import com.android.gpstest.model.DilutionOfPrecision
import com.android.gpstest.model.SatelliteMetadata
import com.android.gpstest.library.data.FixState
import com.android.gpstest.library.model.CoordinateType
import com.android.gpstest.library.model.DilutionOfPrecision
import com.android.gpstest.library.model.SatelliteMetadata
import com.android.gpstest.library.util.*
import com.android.gpstest.ui.components.LinkifyText
import com.android.gpstest.util.*
import com.android.gpstest.util.FormatUtils.formatBearing
import com.android.gpstest.util.FormatUtils.formatBearingAccuracy
import com.android.gpstest.util.PreferenceUtil.coordinateFormat
import com.android.gpstest.util.PreferenceUtil.shareIncludeAltitude
import com.android.gpstest.util.PreferenceUtils.gnssFilter
import com.android.gpstest.util.SatelliteUtil.isVerticalAccuracySupported
import com.android.gpstest.library.util.FormatUtils.formatBearing
import com.android.gpstest.library.util.FormatUtils.formatBearingAccuracy
import com.android.gpstest.library.util.PreferenceUtil.coordinateFormat
import com.android.gpstest.library.util.PreferenceUtil.shareIncludeAltitude
import com.android.gpstest.library.util.PreferenceUtils.gnssFilter
import com.android.gpstest.library.util.SatelliteUtil.isVerticalAccuracySupported
import java.text.SimpleDateFormat
@Preview
@@ -101,7 +103,6 @@ fun previewLocation(): Location {
return l
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun LocationCard(
location: Location,
@@ -162,32 +163,32 @@ fun ValueColumn1(
@Composable
fun Latitude(location: Location) {
LocationValue(FormatUtils.formatLatOrLon(location.latitude, CoordinateType.LATITUDE))
LocationValue(FormatUtils.formatLatOrLon(app, location.latitude, CoordinateType.LATITUDE, prefs))
}
@Composable
fun Longitude(location: Location) {
LocationValue(FormatUtils.formatLatOrLon(location.longitude, CoordinateType.LONGITUDE))
LocationValue(FormatUtils.formatLatOrLon(app, location.longitude, CoordinateType.LONGITUDE, prefs))
}
@Composable
fun Altitude(location: Location) {
LocationValue(FormatUtils.formatAltitude(location))
LocationValue(FormatUtils.formatAltitude(app, location, prefs))
}
@Composable
fun AltitudeMsl(altitudeMsl: Double) {
LocationValue(FormatUtils.formatAltitudeMsl(altitudeMsl))
LocationValue(FormatUtils.formatAltitudeMsl(app, altitudeMsl, prefs))
}
@Composable
fun Speed(location: Location) {
LocationValue(FormatUtils.formatSpeed(location))
LocationValue(FormatUtils.formatSpeed(app, location, prefs))
}
@Composable
fun SpeedAccuracy(location: Location) {
LocationValue(FormatUtils.formatSpeedAccuracy(location))
LocationValue(FormatUtils.formatSpeedAccuracy(app, location, prefs))
}
@Composable
@@ -226,7 +227,7 @@ fun ValueColumn2(
@Composable
fun Time(location: Location) {
if (location.time == 0L || !PreferenceUtils.isTrackingStarted()) {
if (location.time == 0L || !PreferenceUtils.isTrackingStarted(prefs)) {
LocationValue("")
} else {
formatTime(location.time)
@@ -285,12 +286,12 @@ fun TTFF(ttff: String) {
*/
@Composable
fun Accuracy(location: Location) {
LocationValue(FormatUtils.formatAccuracy(location))
LocationValue(FormatUtils.formatAccuracy(app, location, prefs))
}
@Composable
fun NumSats(satelliteMetadata: SatelliteMetadata) {
val fontStyle = if (gnssFilter().isNotEmpty()) {
val fontStyle = if (gnssFilter(app, prefs).isNotEmpty()) {
// Make text italic so it matches filter text
FontStyle.Italic
} else {
@@ -310,7 +311,7 @@ fun NumSats(satelliteMetadata: SatelliteMetadata) {
@Composable
fun Bearing(location: Location) {
if (location.hasBearing()) {
LocationValue(formatBearing(location))
LocationValue(formatBearing(app, location))
} else {
LocationValue("")
}
@@ -318,7 +319,7 @@ fun Bearing(location: Location) {
@Composable
fun BearingAccuracy(location: Location) {
LocationValue(formatBearingAccuracy(location))
LocationValue(formatBearingAccuracy(app, location))
}
@Composable
@@ -507,12 +508,12 @@ fun LockIcon(fixState: FixState) {
private fun copyToClipboard(context: Context, location: Location) {
if (location.latitude == 0.0 && location.longitude == 0.0) return
val formattedLocation = UIUtils.formatLocationForDisplay(
location, null, shareIncludeAltitude(),
null, null, null, coordinateFormat()
val formattedLocation = LibUIUtils.formatLocationForDisplay(
app, location, null, shareIncludeAltitude(app, prefs),
null, null, null, coordinateFormat(app, prefs)
)
if (!TextUtils.isEmpty(formattedLocation)) {
IOUtils.copyToClipboard(formattedLocation)
IOUtils.copyToClipboard(app, formattedLocation)
// Android 12 and higher generates a Toast automatically
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_LONG)

View File

@@ -18,24 +18,21 @@ package com.android.gpstest.ui.status
import android.os.Bundle
import android.view.*
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.android.gpstest.Application.Companion.app
import com.android.gpstest.Application.Companion.prefs
import com.android.gpstest.R
import com.android.gpstest.ui.SignalInfoViewModel
import com.android.gpstest.library.ui.SignalInfoViewModel
import com.android.gpstest.ui.theme.AppTheme
import com.android.gpstest.util.PreferenceUtil.darkTheme
import com.android.gpstest.library.util.PreferenceUtil.darkTheme
import com.android.gpstest.util.UIUtils.showSortByDialog
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.ExperimentalCoroutinesApi
@AndroidEntryPoint
class StatusFragment : Fragment() {
@ExperimentalFoundationApi
@ExperimentalCoroutinesApi
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@@ -47,7 +44,7 @@ class StatusFragment : Fragment() {
setContent {
AppTheme(
// TODO - add "system dark setting" as an option in preferences - Issue #277
darkTheme = darkTheme()
darkTheme = darkTheme(app, prefs)
) {
StatusScreen(viewModel = viewModel)
}

View File

@@ -41,18 +41,17 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.android.gpstest.Application.Companion.app
import com.android.gpstest.Application.Companion.prefs
import com.android.gpstest.R
import com.android.gpstest.data.FixState
import com.android.gpstest.model.*
import com.android.gpstest.ui.SignalInfoViewModel
import com.android.gpstest.util.CarrierFreqUtils
import com.android.gpstest.util.MathUtils
import com.android.gpstest.util.PreferenceUtils
import com.android.gpstest.util.PreferenceUtils.gnssFilter
import kotlinx.coroutines.ExperimentalCoroutinesApi
import com.android.gpstest.library.data.FixState
import com.android.gpstest.library.model.*
import com.android.gpstest.library.ui.SignalInfoViewModel
import com.android.gpstest.library.util.CarrierFreqUtils
import com.android.gpstest.library.util.MathUtils
import com.android.gpstest.library.util.PreferenceUtils
import com.android.gpstest.library.util.PreferenceUtils.gnssFilter
@ExperimentalCoroutinesApi
@ExperimentalFoundationApi
@Composable
fun StatusScreen(viewModel: SignalInfoViewModel) {
//
@@ -62,7 +61,9 @@ fun StatusScreen(viewModel: SignalInfoViewModel) {
val ttff: String by viewModel.ttff.observeAsState("")
val altitudeMsl: Double by viewModel.altitudeMsl.observeAsState(Double.NaN)
val dop: DilutionOfPrecision by viewModel.dop.observeAsState(DilutionOfPrecision(Double.NaN,Double.NaN,Double.NaN))
val satelliteMetadata: SatelliteMetadata by viewModel.filteredSatelliteMetadata.observeAsState(SatelliteMetadata())
val satelliteMetadata: SatelliteMetadata by viewModel.filteredSatelliteMetadata.observeAsState(
SatelliteMetadata()
)
val fixState: FixState by viewModel.fixState.observeAsState(FixState.NotAcquired)
val gnssStatuses: List<SatelliteStatus> by viewModel.filteredGnssStatuses.observeAsState(emptyList())
val sbasStatuses: List<SatelliteStatus> by viewModel.filteredSbasStatuses.observeAsState(emptyList())
@@ -81,8 +82,8 @@ fun StatusScreen(viewModel: SignalInfoViewModel) {
dop,
satelliteMetadata,
fixState)
if (gnssFilter().isNotEmpty()) {
Filter(allStatuses.size, satelliteMetadata) { PreferenceUtils.clearGnssFilter() }
if (gnssFilter(app, prefs).isNotEmpty()) {
Filter(allStatuses.size, satelliteMetadata) { PreferenceUtils.clearGnssFilter(app, prefs) }
}
GnssStatusCard(gnssStatuses)
SbasStatusCard(sbasStatuses)

View File

@@ -1,286 +0,0 @@
package com.android.gpstest.util
import android.content.Context
import android.content.SharedPreferences
import android.location.GnssMeasurementsEvent
import android.location.LocationManager
import android.os.Build
import com.android.gpstest.Application
import com.android.gpstest.R
/**
* Provides access to SharedPreferences to Activities and Services.
*/
internal object PreferenceUtil {
const val SECONDS_TO_MILLISECONDS = 1000
val METERS =
Application.app.resources.getStringArray(R.array.preferred_distance_units_values)[0]
val METERS_PER_SECOND =
Application.app.resources.getStringArray(R.array.preferred_speed_units_values)[0]
val KILOMETERS_PER_HOUR =
Application.app.resources.getStringArray(R.array.preferred_speed_units_values)[1]
/**
* Returns the minTime between location updates used for the LocationListener in milliseconds
*/
fun minTimeMillis(): Long {
val minTimeDouble: Double =
Application.prefs
.getString(Application.app.getString(R.string.pref_key_gps_min_time), "1")
?.toDouble() ?: 1.0
return (minTimeDouble * SECONDS_TO_MILLISECONDS).toLong()
}
/**
* Returns the minDistance between location updates used for the LocationLitsener in meters
*/
fun minDistance(): Float {
return Application.prefs.getString(Application.app.getString(R.string.pref_key_gps_min_distance), "0") ?.toFloat() ?: 0.0f
}
/**
* Returns true if the user has selected to write locations to file output, false if they have not
*/
fun writeLocationToFile(): Boolean {
return Application.prefs.getBoolean(Application.app.getString(R.string.pref_key_file_location_output), false)
}
fun writeMeasurementToLogcat(): Boolean {
return Application.prefs.getBoolean(Application.app.getString(R.string.pref_key_as_measurement_output), false)
}
fun writeMeasurementsToFile(): Boolean {
return Application.prefs.getBoolean(Application.app.getString(R.string.pref_key_file_measurement_output), false)
}
fun writeNmeaToAndroidMonitor(): Boolean {
return Application.prefs.getBoolean(Application.app.getString(R.string.pref_key_as_nmea_output), true)
}
fun writeNmeaTimestampToLogcat(): Boolean {
return Application.prefs.getBoolean(Application.app.getString(R.string.pref_key_as_nmea_timestamp_output), true)
}
fun writeNmeaToFile(): Boolean {
return Application.prefs.getBoolean(Application.app.getString(R.string.pref_key_file_nmea_output), false)
}
fun writeAntennaInfoToFileJson(): Boolean {
return Application.prefs.getBoolean(Application.app.getString(R.string.pref_key_file_antenna_output_json), false);
}
fun writeAntennaInfoToFileCsv(): Boolean {
return Application.prefs.getBoolean(Application.app.getString(R.string.pref_key_file_antenna_output_csv), false)
}
fun writeNavMessageToLogcat(): Boolean {
return Application.prefs.getBoolean(Application.app.getString(R.string.pref_key_as_navigation_message_output), false);
}
fun writeNavMessageToFile(): Boolean {
return Application.prefs.getBoolean(Application.app.getString(R.string.pref_key_file_navigation_message_output), false);
}
fun writeStatusToFile(): Boolean {
return Application.prefs.getBoolean(Application.app.getString(R.string.pref_key_file_gnss_status_output), false);
}
fun writeOrientationToFile(): Boolean {
return Application.prefs.getBoolean(Application.app.getString(R.string.pref_key_file_orientation_output), false);
}
fun injectTimeWhenLogging(): Boolean {
return Application.prefs.getBoolean(Application.app.getString(R.string.pref_key_inject_time_when_logging), true);
}
fun injectPsdsWhenLogging(): Boolean {
return Application.prefs.getBoolean(Application.app.getString(R.string.pref_key_inject_psds_when_logging), true);
}
fun isFileLoggingEnabled(): Boolean {
return isCsvLoggingEnabled() || isJsonLoggingEnabled()
}
fun isCsvLoggingEnabled(): Boolean {
return writeNmeaToFile() || writeMeasurementsToFile() || writeNavMessageToFile() || writeLocationToFile() || writeAntennaInfoToFileCsv() || writeStatusToFile() || writeOrientationToFile()
}
fun isJsonLoggingEnabled(): Boolean {
return writeAntennaInfoToFileJson()
}
fun distanceUnits(): String {
return Application.prefs.getString(Application.app.getString(R.string.pref_key_preferred_distance_units_v2), METERS) ?: METERS
}
fun speedUnits(): String {
return Application.prefs.getString(Application.app.getString(R.string.pref_key_preferred_speed_units_v2), METERS_PER_SECOND) ?: METERS_PER_SECOND
}
fun coordinateFormat(): String {
return Application.prefs.getString(
Application.app.getString(R.string.pref_key_coordinate_format),
Application.app.getString(R.string.preferences_coordinate_format_dd_key)
) ?: Application.app.getString(R.string.preferences_coordinate_format_dd_key)
}
fun runInBackground(): Boolean {
return Application.prefs.getBoolean(Application.app.getString(R.string.pref_key_gnss_background), false)
}
fun darkTheme(): Boolean {
return Application.prefs.getBoolean(Application.app.getString(R.string.pref_key_dark_theme), false)
}
fun shareIncludeAltitude(): Boolean {
return Application.prefs.getBoolean(
Application.app.getString(R.string.pref_key_share_include_altitude), false
)
}
/**
* Returns true if preferences related to raw measurements should be enabled,
* false if they should be disabled
*/
fun enableMeasurementsPref(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val manager = Application.app.getSystemService(Context.LOCATION_SERVICE) as LocationManager
return SatelliteUtils.isMeasurementsSupported(manager)
}
// Legacy versions before Android S
val capabilityMeasurementsInt = Application.prefs.getInt(
Application.app.getString(R.string.capability_key_raw_measurements),
PreferenceUtils.CAPABILITY_UNKNOWN
)
return capabilityMeasurementsInt != PreferenceUtils.CAPABILITY_NOT_SUPPORTED
}
/**
* Returns true if preferences related to navigation messages should be enabled,
* false if they should be disabled
*/
fun enableNavMessagesPref(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val manager = Application.app.getSystemService(Context.LOCATION_SERVICE) as LocationManager
return SatelliteUtils.isNavMessagesSupported(manager)
}
// Legacy versions before Android S
val capabilityNavMessagesInt = Application.prefs.getInt(
Application.app.getString(R.string.capability_key_nav_messages),
PreferenceUtils.CAPABILITY_UNKNOWN
)
return capabilityNavMessagesInt != PreferenceUtils.CAPABILITY_NOT_SUPPORTED
}
/**
* Saves device capabilities for GNSS measurements and related information from the given [event]
*/
fun saveMeasurementCapabilities(event: GnssMeasurementsEvent) {
var agcSupport = PreferenceUtils.CAPABILITY_UNKNOWN
var carrierPhaseSupport = PreferenceUtils.CAPABILITY_UNKNOWN
// Loop through all measurements - if at least one supports, then mark as supported
for (measurement in event.measurements) {
if (SatelliteUtils.isAutomaticGainControlSupported(measurement)) {
agcSupport = PreferenceUtils.CAPABILITY_SUPPORTED
} else if (agcSupport == PreferenceUtils.CAPABILITY_UNKNOWN) {
agcSupport = PreferenceUtils.CAPABILITY_NOT_SUPPORTED
}
if (SatelliteUtils.isCarrierPhaseSupported(measurement)) {
carrierPhaseSupport = PreferenceUtils.CAPABILITY_SUPPORTED
} else if (carrierPhaseSupport == PreferenceUtils.CAPABILITY_UNKNOWN) {
carrierPhaseSupport = PreferenceUtils.CAPABILITY_NOT_SUPPORTED
}
}
PreferenceUtils.saveInt(
Application.app
.getString(R.string.capability_key_measurement_automatic_gain_control),
agcSupport
)
PreferenceUtils.saveInt(
Application.app
.getString(R.string.capability_key_measurement_delta_range),
carrierPhaseSupport
)
}
/**
* Creates a new preference listener that will invoke the provide [cancelFlows] function (e.g., to cancel jobs)
* when the user turns off tracking via the UI.
*
* Returns a reference to the OnSharedPreferenceChangeListener so it can be held by the calling class, as
* anonymous preference listeners tend to get GC'd by Android.
*/
fun newStopTrackingListener(cancelFlows: () -> Unit): SharedPreferences.OnSharedPreferenceChangeListener {
return SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (key == PreferenceUtils.KEY_SERVICE_TRACKING_ENABLED) {
if (!PreferenceUtils.isTrackingStarted()) {
cancelFlows()
}
}
}
}
/**
* Creates a new preference listener that will invoke the provide [initLogging] function
* when the user turns on file logging via any of the Settings options and all of the other file
* logging settings were previously off
*
* Returns a reference to the OnSharedPreferenceChangeListener so it can be held by the calling class, as
* anonymous preference listeners tend to get GC'd by Android.
*/
fun newFileLoggingListener(initLogging: () -> Unit): SharedPreferences.OnSharedPreferenceChangeListener {
return SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (key == Application.app.getString(R.string.pref_key_file_location_output) ||
key == Application.app.getString(R.string.pref_key_file_measurement_output) ||
key == Application.app.getString(R.string.pref_key_file_nmea_output) ||
key == Application.app.getString(R.string.pref_key_file_navigation_message_output) ||
key == Application.app.getString(R.string.pref_key_file_antenna_output_csv) ||
key == Application.app.getString(R.string.pref_key_file_antenna_output_json)) {
// Count number of file logging preferences that are enabled
val loggingEnabled = arrayOf(writeLocationToFile(), writeMeasurementsToFile(), writeNmeaToFile(), writeNavMessageToFile(), writeAntennaInfoToFileCsv(), writeAntennaInfoToFileJson())
val enabledCount = loggingEnabled.count { it }
if (enabledCount == 1) {
if (key == Application.app.getString(R.string.pref_key_file_location_output) &&
writeLocationToFile()
) {
// Location file logging was just enabled
initLogging()
}
if (key == Application.app.getString(R.string.pref_key_file_measurement_output) &&
writeMeasurementsToFile()
) {
// Measurement file logging was just enabled
initLogging()
}
if (key == Application.app.getString(R.string.pref_key_file_nmea_output) &&
writeNmeaToFile()
) {
// NMEA file logging was just enabled
initLogging()
}
if (key == Application.app.getString(R.string.pref_key_file_navigation_message_output) &&
writeNavMessageToFile()
) {
// Nav message file logging was just enabled
initLogging()
}
if (key == Application.app.getString(R.string.pref_key_file_antenna_output_csv) &&
writeAntennaInfoToFileCsv()
) {
// Antenna CSV file logging was just enabled
initLogging()
}
if (key == Application.app.getString(R.string.pref_key_file_antenna_output_json) &&
writeAntennaInfoToFileJson()
) {
// Antenna JSON file logging was just enabled
initLogging()
}
}
}
}
}
}

View File

@@ -15,609 +15,46 @@
*/
package com.android.gpstest.util
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.app.Activity
import android.app.Dialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import com.android.gpstest.R
import android.location.Location
import android.location.LocationManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.text.Spannable
import android.text.TextUtils
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.widget.CheckBox
import android.widget.CompoundButton
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.android.gpstest.Application
import com.android.gpstest.Application.Companion.app
import com.android.gpstest.Application.Companion.prefs
import com.android.gpstest.BuildConfig
import com.android.gpstest.R
import com.android.gpstest.io.CsvFileLogger
import com.android.gpstest.io.JsonFileLogger
import com.android.gpstest.model.CoordinateType
import com.android.gpstest.model.GnssType
import com.android.gpstest.model.SbasType
import com.android.gpstest.library.model.GnssType
import com.android.gpstest.library.util.IOUtils
import com.android.gpstest.library.util.LocationUtils
import com.android.gpstest.library.util.PreferenceUtils
import com.android.gpstest.library.util.LibUIUtils
import com.android.gpstest.ui.GnssFilterDialog
import com.android.gpstest.ui.HelpActivity
import com.android.gpstest.ui.SignalInfoViewModel
import com.android.gpstest.ui.share.ShareDialogFragment
import com.android.gpstest.ui.share.ShareDialogFragment.Companion.KEY_ALTERNATE_FILE_URI
import com.android.gpstest.ui.share.ShareDialogFragment.Companion.KEY_LOCATION
import com.android.gpstest.ui.share.ShareDialogFragment.Companion.KEY_LOGGING_ENABLED
import com.android.gpstest.ui.share.ShareDialogFragment.Companion.KEY_LOG_FILES
import com.android.gpstest.view.GpsSkyView
import com.google.android.material.chip.Chip
import java.io.File
import java.math.BigDecimal
import java.math.RoundingMode
import java.util.*
/**
* Utilities for processing user inteface elements
*/
internal object UIUtils {
const val TAG = "UIUtils"
var PICKFILE_REQUEST_CODE = 101
const val ANIMATION_DURATION_SHORT_MS = 200
const val ANIMATION_DURATION_MEDIUM_MS = 400
const val ANIMATION_DURATION_LONG_MS = 500
// Dialogs
const val WHATSNEW_DIALOG = 1
const val HELP_DIALOG = 2
const val CLEAR_ASSIST_WARNING_DIALOG = 3
private const val WHATS_NEW_VER = "whatsNewVer"
/**
* Formats a view so it is ignored for accessible access
*/
@JvmStatic
fun setAccessibilityIgnore(view: View) {
view.isClickable = false
view.isFocusable = false
view.contentDescription = ""
view.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
}
/**
* Converts screen dimension units from dp to pixels, based on algorithm defined in
* http://developer.android.com/guide/practices/screens_support.html#dips-pels
*
* @param dp value in dp
* @return value in pixels
*/
@JvmStatic
fun dpToPixels(context: Context, dp: Float): Int {
// Get the screen's density scale
val scale = context.resources.displayMetrics.density
// Convert the dps to pixels, based on density scale
return (dp * scale + 0.5f).toInt()
}
/**
* Returns true if the current display is wide enough to show the GPS date on the Status screen,
* false if the current display is too narrow to fit the GPS date
* @param context
* @return true if the current display is wide enough to show the GPS date on the Status screen,
* false if the current display is too narrow to fit the GPS date
*/
fun isWideEnoughForDate(context: Context): Boolean {
// 450dp is a little larger than the width of a Samsung Galaxy S8+
val WIDTH_THRESHOLD = dpToPixels(context, 450f)
return context.resources.displayMetrics.widthPixels > WIDTH_THRESHOLD
}
/**
* Returns true if the activity is still active and dialogs can be managed (i.e., displayed
* or dismissed), or false if it is not
*
* @param activity Activity to check for displaying/dismissing a dialog
* @return true if the activity is still active and dialogs can be managed, or false if it is
* not
*/
fun canManageDialog(activity: Activity?): Boolean {
return if (activity == null) {
false
} else !activity.isFinishing && !activity.isDestroyed
}
/**
* Returns true if the fragment is attached to the activity, or false if it is not attached
*
* @param f fragment to be tested
* @return true if the fragment is attached to the activity, or false if it is not attached
*/
fun isFragmentAttached(f: Fragment): Boolean {
return f.activity != null && f.isAdded
}
/**
* Converts the provided C/N0 values to a left margin value (dp) for the avg C/N0 indicator ImageViews in gps_sky_signal
* Left margin range for the C/N0 indicator ImageViews in gps_sky_signal is determined by dimens.xml
* cn0_meter_width (based on device screen width) and cn0_indicator_min_left_margin values.
*
* This is effectively an affine transform - https://math.stackexchange.com/a/377174/554287.
*
* @param cn0 carrier-to-noise density at the antenna of the satellite in dB-Hz (from GnssStatus)
* @return left margin value in dp for the C/N0 indicator ImageViews
*/
@JvmStatic
fun cn0ToIndicatorLeftMarginPx(
cn0: Float,
minIndicatorMarginPx: Int,
maxIndicatorMarginPx: Int
): Int {
return MathUtils.mapToRange(
cn0,
GpsSkyView.MIN_VALUE_CN0,
GpsSkyView.MAX_VALUE_CN0,
minIndicatorMarginPx.toFloat(),
maxIndicatorMarginPx.toFloat()
).toInt()
}
/**
* Converts the provided C/N0 values to a left margin value (dp) for the avg C/N0 TextViews in gps_sky_signal
* Left margin range for the C/N0 indicator TextView in gps_sky_signal is determined by dimens.xml
* cn0_meter_width (based on device screen width) and cn0_textview_min_left_margin values.
*
* This is effectively an affine transform - https://math.stackexchange.com/a/377174/554287.
*
* @param cn0 carrier-to-noise density at the antenna of the satellite in dB-Hz (from GnssStatus)
* @return left margin value in dp for the C/N0 TextViews
*/
@JvmStatic
fun cn0ToTextViewLeftMarginPx(
cn0: Float,
minTextViewMarginPx: Int,
maxTextViewMarginPx: Int
): Int {
return MathUtils.mapToRange(
cn0,
GpsSkyView.MIN_VALUE_CN0,
GpsSkyView.MAX_VALUE_CN0,
minTextViewMarginPx.toFloat(),
maxTextViewMarginPx.toFloat()
).toInt()
}
/**
* Sets the margins for a given view
*
* @param v View to set the margin for
* @param l left margin, in pixels
* @param t top margin, in pixels
* @param r right margin, in pixels
* @param b bottom margin, in pixels
*/
fun setMargins(v: View, l: Int, t: Int, r: Int, b: Int) {
val p = v.layoutParams as MarginLayoutParams
p.setMargins(l, t, r, b)
v.layoutParams = p
}
/**
* Opens email apps based on the given email address
* @param email address
* @param location string that shows the current location
* @param signalInfoViewModel view model that contains state of GNSS
*/
fun sendEmail(
context: Context,
email: String,
location: String?,
signalInfoViewModel: SignalInfoViewModel
) {
val locationManager = app.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val pm = context.packageManager
val appInfo: PackageInfo
val body = StringBuilder()
body.append(context.getString(R.string.feedback_body))
var versionName: String? = ""
var versionCode = 0
try {
appInfo = pm.getPackageInfo(
context.packageName,
PackageManager.GET_META_DATA
)
versionName = appInfo.versionName
versionCode = appInfo.versionCode
} catch (e: PackageManager.NameNotFoundException) {
// Leave version as empty string
}
// App version
body.append("App version: v")
.append(versionName)
.append(" (")
.append(versionCode)
.append(
"""
-${BuildConfig.FLAVOR})
""".trimIndent()
)
// Device properties
body.append(
"""
Model: ${Build.MODEL}
""".trimIndent()
)
body.append(
"""Android version: ${Build.VERSION.RELEASE} / ${Build.VERSION.SDK_INT}
"""
)
if (!TextUtils.isEmpty(location)) {
body.append("Location: $location\n")
}
body.append(
"""
GNSS HW year: ${IOUtils.getGnssHardwareYear()}
""".trimIndent()
)
if (!IOUtils.getGnssHardwareModelName().trim { it <= ' ' }.isEmpty()) {
body.append(
"""
GNSS HW name: ${IOUtils.getGnssHardwareModelName()}
""".trimIndent()
)
}
// Raw GNSS measurement capability
var capability = prefs.getInt(
app.getString(R.string.capability_key_raw_measurements),
PreferenceUtils.CAPABILITY_UNKNOWN
)
if (capability != PreferenceUtils.CAPABILITY_UNKNOWN) {
body.append(
app.getString(
R.string.capability_title_raw_measurements,
PreferenceUtils.getCapabilityDescription(capability)
)
)
}
// Navigation messages capability
capability = prefs.getInt(
app.getString(R.string.capability_key_nav_messages),
PreferenceUtils.CAPABILITY_UNKNOWN
)
if (capability != PreferenceUtils.CAPABILITY_UNKNOWN) {
body.append(
app.getString(
R.string.capability_title_nav_messages,
PreferenceUtils.getCapabilityDescription(capability)
)
)
}
// NMEA capability
capability = prefs.getInt(
app.getString(R.string.capability_key_nmea),
PreferenceUtils.CAPABILITY_UNKNOWN
)
if (capability != PreferenceUtils.CAPABILITY_UNKNOWN) {
body.append(
app.getString(
R.string.capability_title_nmea,
PreferenceUtils.getCapabilityDescription(capability)
)
)
}
// Inject PSDS capability
capability = prefs.getInt(
app.getString(R.string.capability_key_inject_psds),
PreferenceUtils.CAPABILITY_UNKNOWN
)
if (capability != PreferenceUtils.CAPABILITY_UNKNOWN) {
body.append(
app.getString(
R.string.capability_title_inject_psds,
PreferenceUtils.getCapabilityDescription(capability)
)
)
}
// Inject time capability
capability = prefs.getInt(
app.getString(R.string.capability_key_inject_time),
PreferenceUtils.CAPABILITY_UNKNOWN
)
if (capability != PreferenceUtils.CAPABILITY_UNKNOWN) {
body.append(
app.getString(
R.string.capability_title_inject_time,
PreferenceUtils.getCapabilityDescription(capability)
)
)
}
// Delete assist capability
capability = prefs.getInt(
app.getString(R.string.capability_key_delete_assist),
PreferenceUtils.CAPABILITY_UNKNOWN
)
if (capability != PreferenceUtils.CAPABILITY_UNKNOWN) {
body.append(
app.getString(
R.string.capability_title_delete_assist,
PreferenceUtils.getCapabilityDescription(capability)
)
)
}
// Got fix
body.append(
app.getString(
R.string.capability_title_got_fix,
location != null && signalInfoViewModel.gotFirstFix()
)
)
// We need a fix to determine these attributes reliably
if (location != null && signalInfoViewModel.gotFirstFix()) {
// Dual frequency
body.append(
app.getString(
R.string.capability_title_dual_frequency,
PreferenceUtils.getCapabilityDescription(signalInfoViewModel.isNonPrimaryCarrierFreqInView)
)
)
// Supported GNSS
val gnss: List<GnssType> = ArrayList(signalInfoViewModel.getSupportedGnss())
Collections.sort(gnss)
body.append(
app.getString(
R.string.capability_title_supported_gnss, IOUtils.trimEnds(
IOUtils.replaceNavstar(gnss.toString())
)
)
)
// GNSS CF
val gnssCfs: List<String> = ArrayList(signalInfoViewModel.getSupportedGnssCfs())
if (!gnssCfs.isEmpty()) {
Collections.sort(gnssCfs)
body.append(
app.getString(
R.string.capability_title_gnss_cf,
IOUtils.trimEnds(gnssCfs.toString())
)
)
}
// Supported SBAS
val sbas: List<SbasType> = ArrayList(signalInfoViewModel.getSupportedSbas())
if (!sbas.isEmpty()) {
Collections.sort(sbas)
body.append(
app.getString(
R.string.capability_title_supported_sbas,
IOUtils.trimEnds(sbas.toString())
)
)
}
// SBAS CF
val sbasCfs: List<String> = ArrayList(signalInfoViewModel.getSupportedSbasCfs())
if (!sbasCfs.isEmpty()) {
Collections.sort(sbasCfs)
body.append(
app.getString(
R.string.capability_title_sbas_cf,
IOUtils.trimEnds(sbasCfs.toString())
)
)
}
// Accumulated delta range
body.append(
app.getString(
R.string.capability_title_accumulated_delta_range,
PreferenceUtils.getCapabilityDescription(
prefs.getInt(
app.getString(R.string.capability_key_measurement_delta_range),
PreferenceUtils.CAPABILITY_UNKNOWN
)
)
)
)
// Automatic gain control
body.append(
app.getString(
R.string.capability_title_automatic_gain_control,
PreferenceUtils.getCapabilityDescription(
prefs.getInt(
app.getString(R.string.capability_key_measurement_automatic_gain_control),
PreferenceUtils.CAPABILITY_UNKNOWN
)
)
)
)
}
// GNSS Antenna Info
val gnssAntennaInfo = app.getString(
R.string.capability_title_gnss_antenna_info,
PreferenceUtils.getCapabilityDescription(
SatelliteUtils.isGnssAntennaInfoSupported(
locationManager
)
)
)
body.append(gnssAntennaInfo)
if (gnssAntennaInfo == app.getString(R.string.capability_value_supported)) {
body.append(
app.getString(
R.string.capability_title_num_antennas, PreferenceUtils.getInt(
app.getString(R.string.capability_key_num_antenna), -1
)
)
)
body.append(
app.getString(
R.string.capability_title_antenna_cfs, PreferenceUtils.getString(
app.getString(R.string.capability_key_antenna_cf)
)
)
)
}
if (!TextUtils.isEmpty(BuildUtils.getPlayServicesVersion())) {
body.append(
"""
${BuildUtils.getPlayServicesVersion()}
""".trimIndent()
)
}
body.append("\n\n\n")
val send = Intent(Intent.ACTION_SENDTO)
send.data = Uri.parse("mailto:")
send.putExtra(Intent.EXTRA_EMAIL, arrayOf(email))
val subject = context.getString(R.string.feedback_subject)
send.putExtra(Intent.EXTRA_SUBJECT, subject)
send.putExtra(Intent.EXTRA_TEXT, body.toString())
try {
context.startActivity(Intent.createChooser(send, subject))
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.feedback_error, Toast.LENGTH_LONG)
.show()
}
}
/**
* Returns the provided latitude or longitude value in Degrees Minutes Seconds (DMS) format
* @param coordinate latitude or longitude to convert to DMS format
* @param coordinateType whether the coordinate is latitude or longitude
* @return the provided latitude or longitude value in Degrees Minutes Seconds (DMS) format
*/
@JvmStatic
fun getDMSFromLocation(
context: Context,
coordinate: Double,
coordinateType: CoordinateType
): String {
val loc = BigDecimal(coordinate)
val degrees = loc.setScale(0, RoundingMode.DOWN)
val minTemp = loc.subtract(degrees).multiply(BigDecimal(60)).abs()
val minutes = minTemp.setScale(0, RoundingMode.DOWN)
val seconds =
minTemp.subtract(minutes).multiply(BigDecimal(60)).setScale(2, RoundingMode.HALF_UP)
val hemisphere: String
val output_string: Int
if (coordinateType == CoordinateType.LATITUDE) {
hemisphere = if (coordinate < 0) "S" else "N"
output_string = R.string.gps_lat_dms_value
} else {
hemisphere = if (coordinate < 0) "W" else "E"
output_string = R.string.gps_lon_dms_value
}
return context.getString(
output_string,
hemisphere,
degrees.abs().toInt(),
minutes.toInt(),
seconds.toFloat()
)
}
/**
* Returns the provided latitude or longitude value in Decimal Degree Minutes (DDM) format
*
* @param coordinate latitude or longitude to convert to DDM format
* @param coordinateType lat or lon to format hemisphere
* @return the provided latitude or longitude value in Decimal Degree Minutes (DDM) format
*/
@JvmStatic
fun getDDMFromLocation(
context: Context,
coordinate: Double,
coordinateType: CoordinateType
): String {
val loc = BigDecimal(coordinate)
val degrees = loc.setScale(0, RoundingMode.DOWN)
val minutes =
loc.subtract(degrees).multiply(BigDecimal(60)).abs().setScale(3, RoundingMode.HALF_UP)
val hemisphere: String
val output_string: Int
if (coordinateType == CoordinateType.LATITUDE) {
hemisphere = if (coordinate < 0) "S" else "N"
output_string = R.string.gps_lat_ddm_value
} else {
hemisphere = if (coordinate < 0) "W" else "E"
output_string = R.string.gps_lon_ddm_value
}
return context.getString(
output_string,
hemisphere,
degrees.abs().toInt(),
minutes.toFloat()
)
}
/**
* Converts the provide value in meters to the corresponding value in feet
* @param meters value in meters to convert to feet
* @return the provided meters value converted to feet
*/
@JvmStatic
fun toFeet(meters: Double): Double {
return meters * 1000.0 / 25.4 / 12.0
}
/**
* Converts the provide value in meters per second to the corresponding value in kilometers per hour
* @param metersPerSecond value in meters per second to convert to kilometers per hour
* @return the provided meters per second value converted to kilometers per hour
*/
@JvmStatic
fun toKilometersPerHour(metersPerSecond: Float): Float {
return metersPerSecond * 3600f / 1000f
}
/**
* Converts the provide value in meters per second to the corresponding value in miles per hour
* @param metersPerSecond value in meters per second to convert to miles per hour
* @return the provided meters per second value converted to miles per hour
*/
@JvmStatic
fun toMilesPerHour(metersPerSecond: Float): Float {
return toKilometersPerHour(metersPerSecond) / 1.6093440f
}
/**
* Sets the vertical bias for a provided view that is within a ConstraintLayout
* @param view view within a ConstraintLayout
* @param bias vertical bias to be used
*/
@JvmStatic
fun setVerticalBias(view: View, bias: Float) {
val params = view.layoutParams as ConstraintLayout.LayoutParams
params.verticalBias = bias
view.layoutParams = params
}
/**
* Tests to see if the provided text latitude, longitude, and altitude values are valid, and if
* not shows an error dialog and returns false, or if yes then returns true
@@ -681,7 +118,8 @@ internal object UIUtils {
// Save the preference
PreferenceUtils.saveBoolean(
app.getString(R.string.pref_key_never_show_qr_code_instructions),
isChecked
isChecked,
Application.prefs
)
}
val builder = AlertDialog.Builder(activity)
@@ -747,7 +185,8 @@ internal object UIUtils {
*/
private fun createBundleForShareDialog(
location: Location?,
loggingEnabled: Boolean, files: ArrayList<File>,
loggingEnabled: Boolean,
files: ArrayList<File>,
alternateFileUri: Uri?
): Bundle {
val bundle = Bundle()
@@ -758,244 +197,6 @@ internal object UIUtils {
return bundle
}
/**
* Returns the provided location based on the provided coordinate format, and sets the provided
* Views (textView, chips) accordingly if views are provided, and returns the string value.
*
* @param location location to be formatted
* @param textView View to be set with the selected coordinateFormat
* @param includeAltitude true if altitude should be included, false if it should not
* @param chipDecimalDegrees View to be set as checked if "dd" is the coordinateFormat
* @param chipDMS View to be set as checked if "dms" is the coordinateFormat
* @param chipDegreesDecimalMin View to be set as checked if "ddm" is the coordinateFormat
* @param coordinateFormat dd, dms, or ddm
* @return the provided location based on the provided coordinate format
*/
fun formatLocationForDisplay(
location: Location?,
textView: TextView?,
includeAltitude: Boolean,
chipDecimalDegrees: Chip?,
chipDMS: Chip?,
chipDegreesDecimalMin: Chip?,
coordinateFormat: String?
): String {
var formattedLocation = ""
when (coordinateFormat) {
"dd" -> {
// Decimal degrees
if (location != null) {
formattedLocation = IOUtils.createLocationShare(location, includeAltitude)
}
if (chipDecimalDegrees != null) {
chipDecimalDegrees.isChecked = true
}
}
"dms" -> {
// Degrees minutes seconds
if (location != null) {
formattedLocation = IOUtils.createLocationShare(
getDMSFromLocation(app, location.latitude, CoordinateType.LATITUDE),
getDMSFromLocation(app, location.longitude, CoordinateType.LONGITUDE),
if (location.hasAltitude() && includeAltitude) location.altitude.toString() else null
)
}
if (chipDMS != null) {
chipDMS.isChecked = true
}
}
"ddm" -> {
// Degrees decimal minutes
if (location != null) {
formattedLocation = IOUtils.createLocationShare(
getDDMFromLocation(app, location.latitude, CoordinateType.LATITUDE),
getDDMFromLocation(app, location.longitude, CoordinateType.LONGITUDE),
if (location.hasAltitude() && includeAltitude) location.altitude.toString() else null
)
}
if (chipDegreesDecimalMin != null) {
chipDegreesDecimalMin.isChecked = true
}
}
else -> {
// Decimal degrees
formattedLocation = IOUtils.createLocationShare(location, includeAltitude)
if (chipDecimalDegrees != null) {
chipDecimalDegrees.isChecked = true
}
}
}
if (textView != null) {
textView.text = formattedLocation
}
return formattedLocation
}
/**
* Resets the activity title so the locale is updated
*
* @param a the activity to reset the title for
*/
@JvmStatic
fun resetActivityTitle(a: Activity) {
try {
val info =
a.packageManager.getActivityInfo(a.componentName, PackageManager.GET_META_DATA)
if (info.labelRes != 0) {
a.setTitle(info.labelRes)
}
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
}
/**
* Returns true if the app is running on a large screen device, false if it is not
*
* @return true if the app is running on a large screen device, false if it is not
*/
fun isLargeScreen(context: Context): Boolean {
return (context.resources.configuration.screenLayout
and Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE
}
/**
* Returns the display name for the given GnssType
* @param context
* @param gnssType
* @return the display name for the given GnssType
*/
fun getGnssDisplayName(context: Context, gnssType: GnssType?): String {
return when (gnssType) {
GnssType.NAVSTAR -> context.resources.getString(R.string.sky_legend_shape_navstar)
GnssType.GALILEO -> context.resources.getString(R.string.sky_legend_shape_galileo)
GnssType.GLONASS -> context.resources.getString(R.string.sky_legend_shape_glonass)
GnssType.BEIDOU -> context.resources.getString(R.string.sky_legend_shape_beidou)
GnssType.QZSS -> context.resources.getString(R.string.sky_legend_shape_qzss)
GnssType.IRNSS -> context.resources.getString(R.string.sky_legend_shape_irnss)
GnssType.SBAS -> context.resources.getString(R.string.sbas)
GnssType.UNKNOWN -> context.resources.getString(R.string.unknown)
else -> context.resources.getString(R.string.unknown)
}
}
fun setClickableSpan(v: TextView, span: ClickableSpan?) {
val text = v.text as Spannable
text.setSpan(span, 0, text.length, 0)
v.movementMethod = LinkMovementMethod.getInstance()
}
fun removeAllClickableSpans(v: TextView) {
val text = v.text as Spannable
val spans = text.getSpans(0, text.length, ClickableSpan::class.java)
for (cs in spans) {
text.removeSpan(cs)
}
}
/**
* Shows a view using animation
*
* @param v View to show
* @param animationDuration duration of animation
*/
fun showViewWithAnimation(v: View, animationDuration: Int) {
if (v.visibility == View.VISIBLE && v.alpha == 1f) {
// View is already visible and not transparent, return without doing anything
return
}
v.clearAnimation()
v.animate().cancel()
if (v.visibility != View.VISIBLE) {
// Set the content view to 0% opacity but visible, so that it is visible
// (but fully transparent) during the animation.
v.alpha = 0f
v.visibility = View.VISIBLE
}
// Animate the content view to 100% opacity, and clear any animation listener set on the view.
v.animate()
.alpha(1f)
.setDuration(animationDuration.toLong())
.setListener(null)
}
/**
* Hides a view using animation
*
* @param v View to hide
* @param animationDuration duration of animation
*/
fun hideViewWithAnimation(v: View, animationDuration: Int) {
if (v.visibility == View.GONE) {
// View is already gone, return without doing anything
return
}
v.clearAnimation()
v.animate().cancel()
// Animate the view to 0% opacity. After the animation ends, set its visibility to GONE as
// an optimization step (it won't participate in layout passes, etc.)
v.animate()
.alpha(0f)
.setDuration(animationDuration.toLong())
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
v.visibility = View.GONE
}
})
}
/**
* Shows the dialog to explain why location permissions are needed
*
* NOTE - this dialog can't be managed under the old dialog framework as the method
* ActivityCompat.shouldShowRequestPermissionRationale() always returns false.
*/
fun showLocationPermissionDialog(activity: Activity) {
val builder = AlertDialog.Builder(activity)
.setTitle(R.string.title_location_permission)
.setMessage(R.string.text_location_permission)
.setCancelable(false)
.setPositiveButton(
R.string.ok
) { dialog: DialogInterface?, which: Int ->
// Request permissions from the user
ActivityCompat.requestPermissions(
activity,
PermissionUtils.REQUIRED_PERMISSIONS,
PermissionUtils.LOCATION_PERMISSION_REQUEST
)
}
.setNegativeButton(
R.string.exit
) { dialog: DialogInterface?, which: Int ->
// Exit app
activity.finish()
}
builder.create().show()
}
/**
* Ask the user if they want to enable GPS, and if so, show them system settings
*/
fun promptEnableGps(activity: Activity) {
AlertDialog.Builder(activity)
.setMessage(app.getString(R.string.enable_gps_message))
.setPositiveButton(
app.getString(R.string.enable_gps_positive_button)
) { dialog: DialogInterface?, which: Int ->
val intent = Intent(
Settings.ACTION_LOCATION_SOURCE_SETTINGS
)
activity.startActivity(intent)
}
.setNegativeButton(
app.getString(R.string.enable_gps_negative_button)
) { dialog: DialogInterface?, which: Int -> }
.show()
}
fun createHelpDialog(activity: Activity): Dialog {
val builder = AlertDialog.Builder(activity)
builder.setTitle(R.string.title_help)
@@ -1005,7 +206,7 @@ internal object UIUtils {
) { dialog: DialogInterface?, which: Int ->
when (which) {
0 -> activity.showDialog(
WHATSNEW_DIALOG
LibUIUtils.WHATSNEW_DIALOG
)
1 -> activity.startActivity(Intent(activity, HelpActivity::class.java))
}
@@ -1022,21 +223,21 @@ internal object UIUtils {
builder.setView(textView)
builder.setNeutralButton(
R.string.main_help_close
) { _: DialogInterface?, _: Int -> activity.dismissDialog(WHATSNEW_DIALOG) }
) { _: DialogInterface?, _: Int -> activity.dismissDialog(LibUIUtils.WHATSNEW_DIALOG) }
return builder.create()
}
fun showFilterDialog(activity: FragmentActivity) {
val gnssTypes = GnssType.values()
val len = gnssTypes.size
val filter = PreferenceUtils.gnssFilter()
val filter = PreferenceUtils.gnssFilter(app, prefs)
val items = arrayOfNulls<String>(len)
val checks = BooleanArray(len)
// For each GnssType, if it is in the enabled list, mark it as checked.
for (i in 0 until len) {
val gnssType = gnssTypes[i]
items[i] = getGnssDisplayName(app, gnssType)
items[i] = LibUIUtils.getGnssDisplayName(app, gnssType)
if (filter.contains(gnssType)) {
checks[i] = true
}
@@ -1057,66 +258,15 @@ internal object UIUtils {
activity
)
builder.setTitle(R.string.menu_option_sort_by)
val currentSatOrder = PreferenceUtils.getSatSortOrderFromPreferences()
val currentSatOrder = PreferenceUtils.getSatSortOrderFromPreferences(app, Application.prefs)
builder.setSingleChoiceItems(
R.array.sort_sats, currentSatOrder
) { dialog: DialogInterface, index: Int ->
setSortByClause(index)
LibUIUtils.setSortByClause(app, index, prefs)
dialog.dismiss()
}
val dialog = builder.create()
dialog.setOwnerActivity(activity)
dialog.show()
}
/**
* Saves the "sort by" order to preferences
*
* @param index the index of R.array.sort_sats that should be set
*/
private fun setSortByClause(index: Int) {
val sortOptions = app.resources.getStringArray(R.array.sort_sats)
PreferenceUtils.saveString(
app.resources.getString(R.string.pref_key_default_sat_sort),
sortOptions[index]
)
}
/**
* Show the "What's New" message if a new version was just installed
*/
fun autoShowWhatsNew(activity: Activity) {
// Get the current app version.
val appInfo: PackageInfo = try {
app.packageManager.getPackageInfo(
app.packageName,
PackageManager.GET_META_DATA
)
} catch (e: PackageManager.NameNotFoundException) {
// Do nothing
return
}
val oldVer = prefs.getInt(WHATS_NEW_VER, 0)
val newVer = appInfo.versionCode
if (oldVer < newVer) {
activity.showDialog(WHATSNEW_DIALOG)
PreferenceUtils.saveInt(WHATS_NEW_VER, appInfo.versionCode)
}
}
/**
* Returns the `location` object as a human readable string for use in a notification summary
*/
fun Location?.toNotificationSummary(): String {
return if (this != null) {
val lat = FormatUtils.formatLatOrLon(latitude, CoordinateType.LATITUDE)
val lon = FormatUtils.formatLatOrLon(longitude, CoordinateType.LONGITUDE)
val alt = FormatUtils.formatAltitude(this)
val speed = FormatUtils.formatSpeed(this)
val bearing = FormatUtils.formatBearing(this)
"$lat $lon $alt | $speed | $bearing"
} else {
"Unknown location"
}
}
}

View File

@@ -1,6 +1,6 @@
package com.android.gpstest.view;
import static com.android.gpstest.model.SatelliteStatus.NO_DATA;
import static com.android.gpstest.library.model.SatelliteStatus.NO_DATA;
import static java.util.Collections.emptyList;
import android.content.Context;
@@ -19,9 +19,9 @@ import androidx.core.content.ContextCompat;
import com.android.gpstest.Application;
import com.android.gpstest.R;
import com.android.gpstest.model.GnssType;
import com.android.gpstest.model.SatelliteStatus;
import com.android.gpstest.util.UIUtils;
import com.android.gpstest.library.model.GnssType;
import com.android.gpstest.library.model.SatelliteStatus;
import com.android.gpstest.library.util.LibUIUtils;
import java.util.List;
@@ -78,7 +78,7 @@ public class GpsSkyView extends View {
private void init(Context context) {
mContext = context;
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
SAT_RADIUS = UIUtils.dpToPixels(context, 5);
SAT_RADIUS = LibUIUtils.dpToPixels(context, 5);
int textColor;
int backgroundColor;
@@ -155,7 +155,7 @@ public class GpsSkyView extends View {
mPrnIdPaint.setColor(textColor);
mPrnIdPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPrnIdPaint
.setTextSize(UIUtils.dpToPixels(getContext(), SAT_RADIUS * PRN_TEXT_SCALE));
.setTextSize(LibUIUtils.dpToPixels(getContext(), SAT_RADIUS * PRN_TEXT_SCALE));
mPrnIdPaint.setAntiAlias(true);
mNotInViewPaint = new Paint();

View File

@@ -40,7 +40,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.android.gpstest.view.VerticalTextView
<com.android.gpstest.library.view.VerticalTextView
android:id="@+id/error_y_axis_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -100,7 +100,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.android.gpstest.view.VerticalTextView
<com.android.gpstest.library.view.VerticalTextView
android:id="@+id/vert_error_y_axis_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View File

@@ -32,14 +32,15 @@ import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.android.gpstest.Application
import com.android.gpstest.R
import com.android.gpstest.data.LocationRepository
import com.android.gpstest.library.data.LocationRepository
import com.android.gpstest.library.util.MathUtils
import com.android.gpstest.library.util.PreferenceUtil
import com.android.gpstest.library.util.PreferenceUtil.newStopTrackingListener
import com.android.gpstest.map.MapConstants
import com.android.gpstest.map.MapViewModelController
import com.android.gpstest.map.MapViewModelController.MapInterface
import com.android.gpstest.map.OnMapClickListener
import com.android.gpstest.util.MapUtils
import com.android.gpstest.util.MathUtils
import com.android.gpstest.util.PreferenceUtil.newStopTrackingListener
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
@@ -87,9 +88,8 @@ class MapFragment : Fragment(), MapInterface {
// Preference listener that will cancel the above flows when the user turns off tracking via UI
private val trackingListener: SharedPreferences.OnSharedPreferenceChangeListener =
newStopTrackingListener { onGnssStopped() }
newStopTrackingListener ({ onGnssStopped() }, Application.prefs)
@ExperimentalCoroutinesApi
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,

View File

@@ -15,6 +15,7 @@
*/
package com.android.gpstest.util
import com.android.gpstest.library.util.DateTimeUtils
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

View File

@@ -19,6 +19,8 @@ import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import com.android.gpstest.library.util.MathUtils;
import org.junit.Test;
public class MathUtilTest {

View File

@@ -15,7 +15,8 @@
*/
package com.android.gpstest.util;
import com.android.gpstest.model.DilutionOfPrecision;
import com.android.gpstest.library.model.DilutionOfPrecision;
import com.android.gpstest.library.util.NmeaUtils;
import org.junit.Test;

View File

@@ -2,6 +2,8 @@ package com.android.gpstest.util;
import static junit.framework.Assert.assertEquals;
import com.android.gpstest.library.util.LibUIUtils;
import org.junit.Test;
public class UIUtilsTest {
@@ -16,15 +18,15 @@ public class UIUtilsTest {
final int MAX_PX = 140;
// CN0 value of 27.5 dB-Hz is 50% between 10 and 45, so output should be halfway between -6px and 140px, which is 70px
marginPx = UIUtils.cn0ToIndicatorLeftMarginPx(27.5f, MIN_PX, MAX_PX);
marginPx = LibUIUtils.cn0ToIndicatorLeftMarginPx(27.5f, MIN_PX, MAX_PX);
assertEquals(67, marginPx);
// CN0 value of 45.0 dB-Hz is 100% of 45, so output should be 100% of 140px, which is 140px
marginPx = UIUtils.cn0ToIndicatorLeftMarginPx(45.0f, MIN_PX, MAX_PX);
marginPx = LibUIUtils.cn0ToIndicatorLeftMarginPx(45.0f, MIN_PX, MAX_PX);
assertEquals(140, marginPx);
// CN0 value of 10.0 dB-Hz is 0% (min value of CN0), so output should be 0% of px range, which is -6
marginPx = UIUtils.cn0ToIndicatorLeftMarginPx(10.0f, MIN_PX, MAX_PX);
marginPx = LibUIUtils.cn0ToIndicatorLeftMarginPx(10.0f, MIN_PX, MAX_PX);
assertEquals(-6, marginPx);
}
@@ -38,15 +40,15 @@ public class UIUtilsTest {
final int MAX_PX = 149;
// CN0 value of 27.5 dB-Hz is 50% between 10 and 45, so output should be halfway between 3px and 149px, which is 76px
marginPx = UIUtils.cn0ToTextViewLeftMarginPx(27.5f, MIN_PX, MAX_PX);
marginPx = LibUIUtils.cn0ToTextViewLeftMarginPx(27.5f, MIN_PX, MAX_PX);
assertEquals(76, marginPx);
// CN0 value of 45.0 dB-Hz is 100% of 149, so output should be 100% of 149px, which is 149px
marginPx = UIUtils.cn0ToTextViewLeftMarginPx(45.0f, MIN_PX, MAX_PX);
marginPx = LibUIUtils.cn0ToTextViewLeftMarginPx(45.0f, MIN_PX, MAX_PX);
assertEquals(149, marginPx);
// CN0 value of 10.0 dB-Hz is 0% (min value of CN0), so output should be 0% of px range, which is 3px
marginPx = UIUtils.cn0ToTextViewLeftMarginPx(10.0f, MIN_PX, MAX_PX);
marginPx = LibUIUtils.cn0ToTextViewLeftMarginPx(10.0f, MIN_PX, MAX_PX);
assertEquals(3, marginPx);
}
@@ -57,11 +59,11 @@ public class UIUtilsTest {
public void testToFeet() {
double meters = 1.0d;
assertEquals(3.2808398950131235d, UIUtils.toFeet(meters));
assertEquals(3.2808398950131235d, LibUIUtils.toFeet(meters));
meters = 30.0d;
assertEquals(98.4251968503937d, UIUtils.toFeet(meters));
assertEquals(98.4251968503937d, LibUIUtils.toFeet(meters));
}
/**
@@ -71,11 +73,11 @@ public class UIUtilsTest {
public void testToKilometersPerHour() {
float metersPerSecond = 1.0f;
assertEquals(3.6f, UIUtils.toKilometersPerHour(metersPerSecond));
assertEquals(3.6f, LibUIUtils.toKilometersPerHour(metersPerSecond));
metersPerSecond = 30.0f;
assertEquals(108.0f, UIUtils.toKilometersPerHour(metersPerSecond));
assertEquals(108.0f, LibUIUtils.toKilometersPerHour(metersPerSecond));
}
/**
@@ -85,10 +87,10 @@ public class UIUtilsTest {
public void testToMilesPerHour() {
float metersPerSecond = 1.0f;
assertEquals(2.2369363f, UIUtils.toMilesPerHour(metersPerSecond));
assertEquals(2.2369363f, LibUIUtils.toMilesPerHour(metersPerSecond));
metersPerSecond = 60.0f;
assertEquals(134.21617f, UIUtils.toMilesPerHour(metersPerSecond));
assertEquals(134.21617f, LibUIUtils.toMilesPerHour(metersPerSecond));
}
}

View File

@@ -1,13 +1,17 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.5.31'
ext.hilt_version = '2.38.1'
ext {
compose_version = '1.2.0-alpha05'
wear_compose_version = '1.0.2'
}
ext.kotlin_version = '1.6.10'
ext.hilt_version = '2.40.1'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.3'
classpath 'com.android.tools.build:gradle:7.3.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// Hilt for dependency injection
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"

View File

@@ -1,6 +1,7 @@
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.configureondemand=true
org.gradle.caching=false
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC
android.enableJetifier=true
android.useAndroidX=true

View File

@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip

1
library/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

69
library/build.gradle Normal file
View File

@@ -0,0 +1,69 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
}
android {
namespace 'com.android.gpstest.library'
compileSdk 33
defaultConfig {
minSdk 24
targetSdk 32
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt'
}
}
lint {
disable "JvmStaticProvidesInObjectDetector", "FieldSiteTargetOnQualifierAnnotation", "ModuleCompanionObjects", "ModuleCompanionObjectsNotInModuleParent"
}
buildFeatures {
dataBinding true
viewBinding true
}
dataBinding {
enabled = true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.6.1'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.4'
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
implementation("javax.inject:javax.inject:1")
implementation 'com.google.dagger:dagger:2.40.1'
kapt 'com.google.dagger:dagger-compiler:2.40.1'
// Integration with ViewModels
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'
// Hilt for dependency injection
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
// Sliding drawer in map view
implementation 'com.sothree.slidinguppanel:library:3.4.0'
// QR Code reader for ground truth locations
implementation 'com.google.zxing:android-integration:3.3.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

View File

21
library/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# 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

View File

@@ -0,0 +1,24 @@
package com.android.library
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.android.gpstest.library.test", appContext.packageName)
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@@ -1,4 +1,4 @@
package com.android.gpstest.data
package com.android.gpstest.library.data
import android.os.Build
import androidx.annotation.RequiresApi

View File

@@ -13,22 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.data
package com.android.gpstest.library.data
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.location.GnssAntennaInfo
import android.location.LocationManager
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import com.android.gpstest.Application
import com.android.gpstest.R
import com.android.gpstest.util.CarrierFreqUtils
import com.android.gpstest.util.IOUtils
import com.android.gpstest.util.PreferenceUtils
import com.android.gpstest.util.hasPermission
import com.android.gpstest.library.R
import com.android.gpstest.library.util.CarrierFreqUtils
import com.android.gpstest.library.util.IOUtils
import com.android.gpstest.library.util.PreferenceUtils
import com.android.gpstest.library.util.hasPermission
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
@@ -48,7 +48,8 @@ private const val TAG = "SharedAntennaManager"
*/
class SharedAntennaManager constructor(
private val context: Context,
externalScope: CoroutineScope
externalScope: CoroutineScope,
prefs: SharedPreferences
) {
@RequiresApi(Build.VERSION_CODES.R)
@ExperimentalCoroutinesApi
@@ -58,8 +59,9 @@ class SharedAntennaManager constructor(
val callback = GnssAntennaInfo.Listener { list: List<GnssAntennaInfo> ->
// Capture capabilities in preferences
PreferenceUtils.saveInt(
Application.app.getString(R.string.capability_key_num_antenna),
list.size
context.getString(R.string.capability_key_num_antenna),
list.size,
prefs
)
val cfs: MutableList<String> = ArrayList(2)
for (info in list) {
@@ -68,8 +70,9 @@ class SharedAntennaManager constructor(
if (cfs.isNotEmpty()) {
cfs.sort()
PreferenceUtils.saveString(
Application.app.getString(R.string.capability_key_antenna_cf),
IOUtils.trimEnds(cfs.toString())
context.getString(R.string.capability_key_antenna_cf),
IOUtils.trimEnds(cfs.toString()),
prefs
)
}
@@ -87,6 +90,7 @@ class SharedAntennaManager constructor(
try {
locationManager.registerAntennaInfoListener(context.mainExecutor, callback)
} catch (e: Exception) {
Log.e(TAG, "Exception in location flow: $e")
close(e) // in case of exception, close the Flow
}

View File

@@ -13,11 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.data
package com.android.gpstest.library.data
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.location.GnssMeasurementRequest
import android.location.GnssMeasurementsEvent
import android.location.LocationManager
@@ -27,12 +28,11 @@ import android.os.Looper
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import com.android.gpstest.Application
import com.android.gpstest.R
import com.android.gpstest.util.PreferenceUtil.saveMeasurementCapabilities
import com.android.gpstest.util.PreferenceUtils
import com.android.gpstest.util.SatelliteUtils
import com.android.gpstest.util.hasPermission
import com.android.gpstest.library.R
import com.android.gpstest.library.util.PreferenceUtil.saveMeasurementCapabilities
import com.android.gpstest.library.util.PreferenceUtils
import com.android.gpstest.library.util.SatelliteUtils
import com.android.gpstest.library.util.hasPermission
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
@@ -47,6 +47,7 @@ private const val TAG = "SharedGnssMeasurementManager"
* and https://github.com/googlecodelabs/kotlin-coroutines/blob/master/ktx-library-codelab/step-06/myktxlibrary/src/main/java/com/example/android/myktxlibrary/LocationUtils.kt
*/
class SharedGnssMeasurementManager constructor(
private var prefs: SharedPreferences,
private val context: Context,
externalScope: CoroutineScope
) {
@@ -61,12 +62,12 @@ class SharedGnssMeasurementManager constructor(
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
// Check explicit support on Android S and higher here - Android R and lower are checked in status callbacks
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
checkMeasurementSupport(locationManager)
checkMeasurementSupport(context, locationManager, prefs)
}
val callback: GnssMeasurementsEvent.Callback =
object : GnssMeasurementsEvent.Callback() {
override fun onGnssMeasurementsReceived(event: GnssMeasurementsEvent) {
saveMeasurementCapabilities(event)
saveMeasurementCapabilities(context, event, prefs)
//Log.d(TAG, "New measurement: $event")
// Send the new measurement to the Flow observers
@@ -78,7 +79,7 @@ class SharedGnssMeasurementManager constructor(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return
}
handleLegacyMeasurementStatus(status)
handleLegacyMeasurementStatus(context, status, prefs)
}
}
@@ -91,11 +92,10 @@ class SharedGnssMeasurementManager constructor(
try {
if (SatelliteUtils.isForceFullGnssMeasurementsSupported()) {
val forceFullMeasurements = Application.prefs
val forceFullMeasurements = prefs
.getBoolean(
Application.app.getString(R.string.pref_key_force_full_gnss_measurements),
true
)
context.getString(R.string.pref_key_force_full_gnss_measurements),
true)
Log.d(TAG, "Force full GNSS measurements = $forceFullMeasurements")
// Request "force full GNSS measurements" explicitly (on <= Android R this is a manual developer setting)
val request = GnssMeasurementRequest.Builder()
@@ -121,6 +121,7 @@ class SharedGnssMeasurementManager constructor(
}
}
} catch (e: Exception) {
Log.e(TAG, "Exception in location flow: $e")
close(e) // in case of exception, close the Flow
}
@@ -141,66 +142,73 @@ class SharedGnssMeasurementManager constructor(
}
}
private fun handleLegacyMeasurementStatus(status: Int) {
private fun handleLegacyMeasurementStatus(context: Context, status: Int, prefs: SharedPreferences) {
// TODO - surface this state message in UI somewhere, like when user returned from Settings like before? For now just disable logging option in Settings, will surface in Dashboard later
val uiStatusMessage: String
when (status) {
GnssMeasurementsEvent.Callback.STATUS_LOCATION_DISABLED -> {
uiStatusMessage =
Application.app.getString(R.string.gnss_measurement_status_loc_disabled)
context.getString(R.string.gnss_measurement_status_loc_disabled)
PreferenceUtils.saveInt(
Application.app.getString(R.string.capability_key_raw_measurements),
PreferenceUtils.CAPABILITY_LOCATION_DISABLED
context.getString(R.string.capability_key_raw_measurements),
PreferenceUtils.CAPABILITY_LOCATION_DISABLED,
prefs
)
}
GnssMeasurementsEvent.Callback.STATUS_NOT_SUPPORTED -> {
uiStatusMessage =
Application.app.getString(R.string.gnss_measurement_status_not_supported)
context.getString(R.string.gnss_measurement_status_not_supported)
PreferenceUtils.saveInt(
Application.app.getString(R.string.capability_key_raw_measurements),
PreferenceUtils.CAPABILITY_NOT_SUPPORTED
context.getString(R.string.capability_key_raw_measurements),
PreferenceUtils.CAPABILITY_NOT_SUPPORTED,
prefs
)
PreferenceUtils.saveInt(
Application.app
.getString(R.string.capability_key_measurement_automatic_gain_control),
PreferenceUtils.CAPABILITY_NOT_SUPPORTED
context.getString(R.string.capability_key_measurement_automatic_gain_control),
PreferenceUtils.CAPABILITY_NOT_SUPPORTED,
prefs
)
PreferenceUtils.saveInt(
Application.app.getString(R.string.capability_key_measurement_delta_range),
PreferenceUtils.CAPABILITY_NOT_SUPPORTED
context.getString(R.string.capability_key_measurement_delta_range),
PreferenceUtils.CAPABILITY_NOT_SUPPORTED,
prefs
)
}
GnssMeasurementsEvent.Callback.STATUS_READY -> {
uiStatusMessage = Application.app.getString(R.string.gnss_measurement_status_ready)
uiStatusMessage = context.getString(R.string.gnss_measurement_status_ready)
PreferenceUtils.saveInt(
Application.app.getString(R.string.capability_key_raw_measurements),
PreferenceUtils.CAPABILITY_SUPPORTED
context.getString(R.string.capability_key_raw_measurements),
PreferenceUtils.CAPABILITY_SUPPORTED,
prefs
)
}
else -> {
uiStatusMessage = Application.app.getString(R.string.gnss_status_unknown)
uiStatusMessage = context.getString(R.string.gnss_status_unknown)
PreferenceUtils.saveInt(
Application.app.getString(R.string.capability_key_raw_measurements),
PreferenceUtils.CAPABILITY_UNKNOWN
context.getString(R.string.capability_key_raw_measurements),
PreferenceUtils.CAPABILITY_UNKNOWN,
prefs
)
}
}
}
@RequiresApi(api = Build.VERSION_CODES.S)
private fun checkMeasurementSupport(lm: LocationManager) {
private fun checkMeasurementSupport(context: Context, lm: LocationManager, prefs: SharedPreferences) {
// TODO - surface this state message in UI somewhere, like when user returned from Settings like before? For now just disable logging option in Settings, will surface in Dashboard later
val uiStatusMessage: String = if (SatelliteUtils.isMeasurementsSupported(lm)) {
PreferenceUtils.saveInt(
Application.app.getString(R.string.capability_key_raw_measurements),
PreferenceUtils.CAPABILITY_SUPPORTED
context.getString(R.string.capability_key_raw_measurements),
PreferenceUtils.CAPABILITY_SUPPORTED,
prefs
)
Application.app.getString(R.string.gnss_measurement_status_ready)
context.getString(R.string.gnss_measurement_status_ready)
} else {
PreferenceUtils.saveInt(
Application.app.getString(R.string.capability_key_raw_measurements),
PreferenceUtils.CAPABILITY_NOT_SUPPORTED
context.getString(R.string.capability_key_raw_measurements),
PreferenceUtils.CAPABILITY_NOT_SUPPORTED,
prefs
)
Application.app.getString(R.string.gnss_measurement_status_not_supported)
context.getString(R.string.gnss_measurement_status_not_supported)
}
}

View File

@@ -13,11 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.data
package com.android.gpstest.library.data
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.location.GnssStatus
import android.location.Location
import android.location.LocationManager
@@ -27,8 +28,8 @@ import android.os.Looper
import android.os.SystemClock
import android.util.Log
import androidx.core.content.ContextCompat
import com.android.gpstest.util.PreferenceUtil.minTimeMillis
import com.android.gpstest.util.hasPermission
import com.android.gpstest.library.util.PreferenceUtil.minTimeMillis
import com.android.gpstest.library.util.hasPermission
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
@@ -45,7 +46,8 @@ private const val TAG = "SharedGnssStatusManager"
*/
class SharedGnssStatusManager constructor(
private val context: Context,
externalScope: CoroutineScope
externalScope: CoroutineScope,
prefs: SharedPreferences
) {
// State of GnssStatus
private val _statusState = MutableStateFlow<GnssStatusState>(GnssStatusState.Stopped)
@@ -80,7 +82,7 @@ class SharedGnssStatusManager constructor(
override fun onSatelliteStatusChanged(status: GnssStatus) {
val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
if (location != null) {
_fixState.value = checkHaveFix(location)
_fixState.value = checkHaveFix(context, location, prefs)
} else {
_fixState.value = FixState.NotAcquired
}
@@ -109,6 +111,7 @@ class SharedGnssStatusManager constructor(
)
}
} catch (e: Exception) {
Log.e(TAG, "Exception in location flow: $e")
close(e) // in case of exception, close the Flow
}
@@ -135,10 +138,10 @@ class SharedGnssStatusManager constructor(
}
}
private fun checkHaveFix(location: Location): FixState {
val threshold = if (minTimeMillis() >= 1000L) {
private fun checkHaveFix(context: Context, location: Location, prefs: SharedPreferences): FixState {
val threshold = if (minTimeMillis(context, prefs) >= 1000L) {
// Use two requested update intervals (it missed two updates)
TimeUnit.MILLISECONDS.toNanos(minTimeMillis() * 2)
TimeUnit.MILLISECONDS.toNanos(minTimeMillis(context, prefs) * 2)
} else {
// Most Android devices can't refresh faster than 1Hz, so use 1.5 seconds - see #544
TimeUnit.MILLISECONDS.toNanos(1500)

View File

@@ -13,18 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.data
package com.android.gpstest.library.data
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.util.Log
import com.android.gpstest.util.PreferenceUtil.minDistance
import com.android.gpstest.util.PreferenceUtil.minTimeMillis
import com.android.gpstest.util.hasPermission
import com.android.gpstest.library.util.PreferenceUtil.minDistance
import com.android.gpstest.library.util.PreferenceUtil.minTimeMillis
import com.android.gpstest.library.util.hasPermission
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
@@ -40,7 +41,8 @@ private const val TAG = "SharedLocationManager"
*/
class SharedLocationManager constructor(
private val context: Context,
externalScope: CoroutineScope
externalScope: CoroutineScope,
prefs:SharedPreferences
) {
private val _receivingLocationUpdates: MutableStateFlow<Boolean> =
MutableStateFlow(false)
@@ -61,18 +63,19 @@ class SharedLocationManager constructor(
!context.hasPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
) close()
Log.d(TAG, "Starting location updates with minTime=${minTimeMillis()}ms and minDistance=${minDistance()}m")
Log.d(TAG, "Starting location updates with minTime=${minTimeMillis(context, prefs)}ms and minDistance=${minDistance(context, prefs)}m")
_receivingLocationUpdates.value = true
try {
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
minTimeMillis(),
minDistance(),
minTimeMillis(context, prefs),
minDistance(context, prefs),
callback,
context.mainLooper
)
} catch (e: Exception) {
Log.e(TAG, "Exception in location flow: $e")
close(e) // in case of exception, close the Flow
}

View File

@@ -13,11 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.data
package com.android.gpstest.library.data
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.location.GnssNavigationMessage
import android.location.LocationManager
import android.os.Build
@@ -26,11 +27,10 @@ import android.os.Looper
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import com.android.gpstest.Application
import com.android.gpstest.R
import com.android.gpstest.util.PreferenceUtils
import com.android.gpstest.util.SatelliteUtils
import com.android.gpstest.util.hasPermission
import com.android.gpstest.library.R
import com.android.gpstest.library.util.PreferenceUtils
import com.android.gpstest.library.util.SatelliteUtils
import com.android.gpstest.library.util.hasPermission
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
@@ -49,7 +49,8 @@ private const val TAG = "SharedNavMessageManager"
*/
class SharedNavMessageManager constructor(
private val context: Context,
externalScope: CoroutineScope
externalScope: CoroutineScope,
prefs: SharedPreferences
) {
@ExperimentalCoroutinesApi
@SuppressLint("MissingPermission")
@@ -57,7 +58,7 @@ class SharedNavMessageManager constructor(
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
// Check explicit support on Android S and higher here - Android R and lower are checked in status callbacks
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
checkNavMessageSupport(locationManager)
checkNavMessageSupport(context, locationManager, prefs)
}
val callback: GnssNavigationMessage.Callback =
object : GnssNavigationMessage.Callback() {
@@ -72,7 +73,7 @@ class SharedNavMessageManager constructor(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return
}
handleLegacyNavMessageStatus(status)
handleLegacyNavMessageStatus(context, status, prefs)
}
}
@@ -96,6 +97,7 @@ class SharedNavMessageManager constructor(
)
}
} catch (e: Exception) {
Log.e(TAG, "Exception in location flow: $e")
close(e) // in case of exception, close the Flow
}
@@ -116,49 +118,54 @@ class SharedNavMessageManager constructor(
}
@RequiresApi(api = Build.VERSION_CODES.S)
private fun checkNavMessageSupport(locationManager: LocationManager) {
private fun checkNavMessageSupport(context: Context,locationManager: LocationManager, prefs: SharedPreferences) {
// TODO - surface this status message in UI somewhere, like when user returned from Settings like before? For now just disable logging option in Settings, will surface in Dashboard later
val uiStatusMessage: String
uiStatusMessage = if (SatelliteUtils.isNavMessagesSupported(locationManager)) {
PreferenceUtils.saveInt(
Application.app.getString(R.string.capability_key_nav_messages),
PreferenceUtils.CAPABILITY_SUPPORTED
context.getString(R.string.capability_key_nav_messages),
PreferenceUtils.CAPABILITY_SUPPORTED,
prefs
)
Application.app.getString(R.string.gnss_nav_msg_status_ready)
context.getString(R.string.gnss_nav_msg_status_ready)
} else {
PreferenceUtils.saveInt(
Application.app.getString(R.string.capability_key_nav_messages),
PreferenceUtils.CAPABILITY_NOT_SUPPORTED
context.getString(R.string.capability_key_nav_messages),
PreferenceUtils.CAPABILITY_NOT_SUPPORTED,
prefs
)
Application.app.getString(R.string.gnss_nav_msg_status_not_supported)
context.getString(R.string.gnss_nav_msg_status_not_supported)
}
}
private fun handleLegacyNavMessageStatus(status: Int) {
private fun handleLegacyNavMessageStatus(context: Context, status: Int, prefs: SharedPreferences) {
val uiStatusMessage: String
when (status) {
GnssNavigationMessage.Callback.STATUS_LOCATION_DISABLED -> {
uiStatusMessage = Application.app.getString(R.string.gnss_nav_msg_status_loc_disabled)
uiStatusMessage = context.getString(R.string.gnss_nav_msg_status_loc_disabled)
PreferenceUtils.saveInt(
Application.app.getString(R.string.capability_key_nav_messages),
PreferenceUtils.CAPABILITY_LOCATION_DISABLED
context.getString(R.string.capability_key_nav_messages),
PreferenceUtils.CAPABILITY_LOCATION_DISABLED,
prefs
)
}
GnssNavigationMessage.Callback.STATUS_NOT_SUPPORTED -> {
uiStatusMessage = Application.app.getString(R.string.gnss_nav_msg_status_not_supported)
uiStatusMessage = context.getString(R.string.gnss_nav_msg_status_not_supported)
PreferenceUtils.saveInt(
Application.app.getString(R.string.capability_key_nav_messages),
PreferenceUtils.CAPABILITY_NOT_SUPPORTED
context.getString(R.string.capability_key_nav_messages),
PreferenceUtils.CAPABILITY_NOT_SUPPORTED,
prefs
)
}
GnssNavigationMessage.Callback.STATUS_READY -> {
uiStatusMessage = Application.app.getString(R.string.gnss_nav_msg_status_ready)
uiStatusMessage = context.getString(R.string.gnss_nav_msg_status_ready)
PreferenceUtils.saveInt(
Application.app.getString(R.string.capability_key_nav_messages),
PreferenceUtils.CAPABILITY_SUPPORTED
context.getString(R.string.capability_key_nav_messages),
PreferenceUtils.CAPABILITY_SUPPORTED,
prefs
)
}
else -> uiStatusMessage = Application.app.getString(R.string.gnss_status_unknown)
else -> uiStatusMessage = context.getString(R.string.gnss_status_unknown)
}
Log.d(
TAG,

View File

@@ -13,11 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.data
package com.android.gpstest.library.data
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.location.LocationManager
import android.location.OnNmeaMessageListener
import android.os.Build
@@ -25,11 +26,10 @@ import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.content.ContextCompat
import com.android.gpstest.Application
import com.android.gpstest.R
import com.android.gpstest.model.NmeaWithTime
import com.android.gpstest.util.PreferenceUtils
import com.android.gpstest.util.hasPermission
import com.android.gpstest.library.R
import com.android.gpstest.library.model.NmeaWithTime
import com.android.gpstest.library.util.PreferenceUtils
import com.android.gpstest.library.util.hasPermission
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
@@ -48,7 +48,8 @@ private const val TAG = "SharedNmeaManager"
*/
class SharedNmeaManager constructor(
private val context: Context,
externalScope: CoroutineScope
externalScope: CoroutineScope,
prefs: SharedPreferences
) {
@ExperimentalCoroutinesApi
@SuppressLint("MissingPermission")
@@ -56,9 +57,9 @@ class SharedNmeaManager constructor(
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val callback = OnNmeaMessageListener { message: String, timestamp: Long ->
PreferenceUtils.saveInt(
Application.app
.getString(R.string.capability_key_nmea),
PreferenceUtils.CAPABILITY_SUPPORTED
context.getString(R.string.capability_key_nmea),
PreferenceUtils.CAPABILITY_SUPPORTED,
prefs
)
val nmeaWithTime = NmeaWithTime(timestamp, message)
//Log.d(TAG, "New nmea: ${nmeaWithTime}")
@@ -79,6 +80,7 @@ class SharedNmeaManager constructor(
locationManager.addNmeaListener(callback, Handler(Looper.getMainLooper()))
}
} catch (e: Exception) {
Log.e(TAG, "Exception in location flow: $e")
close(e) // in case of exception, close the Flow
}

View File

@@ -13,23 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.data
package com.android.gpstest.library.data
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.hardware.*
import android.hardware.display.DisplayManager
import android.location.LocationManager
import android.util.Log
import android.view.Display
import android.view.Surface
import com.android.gpstest.Application
import com.android.gpstest.R
import com.android.gpstest.model.Orientation
import com.android.gpstest.util.MathUtils
import com.android.gpstest.util.SatelliteUtils
import com.android.gpstest.util.hasPermission
import com.android.gpstest.library.R
import com.android.gpstest.library.model.Orientation
import com.android.gpstest.library.util.MathUtils
import com.android.gpstest.library.util.SatelliteUtils
import com.android.gpstest.library.util.hasPermission
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
@@ -50,6 +50,7 @@ private const val TAG = "SharedSensorManager"
* and https://github.com/googlecodelabs/kotlin-coroutines/blob/master/ktx-library-codelab/step-06/myktxlibrary/src/main/java/com/example/android/myktxlibrary/LocationUtils.kt
*/
class SharedSensorManager constructor(
private val prefs: SharedPreferences,
private val context: Context,
externalScope: CoroutineScope
) {
@@ -100,8 +101,8 @@ class SharedSensorManager constructor(
}
// Correct for true north, if preference is set
if (::geomagneticField.isInitialized && Application.prefs.getBoolean(
Application.app.getString(R.string.pref_key_true_north),
if (::geomagneticField.isInitialized && prefs.getBoolean(
context.getString(R.string.pref_key_true_north),
true
)
) {
@@ -124,7 +125,7 @@ class SharedSensorManager constructor(
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
try {
if (SatelliteUtils.isRotationVectorSensorSupported(Application.app)) {
if (SatelliteUtils.isRotationVectorSensorSupported(context)) {
// Use the modern rotation vector sensors
val vectorSensor: Sensor =
sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
@@ -143,6 +144,7 @@ class SharedSensorManager constructor(
)
}
} catch (e: Exception) {
Log.e(TAG, "Exception in location flow: $e")
close(e) // in case of exception, close the Flow
}
@@ -208,7 +210,7 @@ class SharedSensorManager constructor(
private fun getDisplay(): Display? {
val displayManager =
Application.app.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
return displayManager.getDisplay(0)
}

View File

@@ -0,0 +1,89 @@
package com.android.gpstest.library.di
import android.content.Context
import android.content.SharedPreferences
import android.preference.PreferenceManager
import com.android.gpstest.library.data.*
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.GlobalScope
import javax.inject.Singleton
/**
* Configuration for DI on the repository and shared location manager
*/
@Module
@InstallIn(SingletonComponent::class)
object DataModule {
@Provides
@Singleton
fun provideSharedPreferences(@ApplicationContext context: Context
):SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
@Provides
@Singleton
fun provideSharedLocationManager(
@ApplicationContext context: Context,
prefs: SharedPreferences
): SharedLocationManager =
SharedLocationManager(context, GlobalScope, prefs)
@Provides
fun provideContext(
@ApplicationContext context: Context,
): Context {
return context
}
@Provides
@Singleton
fun provideSharedGnssStatusManager(
@ApplicationContext context: Context,
prefs: SharedPreferences,
): SharedGnssStatusManager =
SharedGnssStatusManager(context, GlobalScope, prefs)
@Provides
@Singleton
fun provideSharedNmeaManager(
@ApplicationContext context: Context,
prefs: SharedPreferences,
): SharedNmeaManager =
SharedNmeaManager(context, GlobalScope, prefs)
@Provides
@Singleton
fun provideSharedSensorManager(
prefs: SharedPreferences,
@ApplicationContext context: Context
): SharedSensorManager =
SharedSensorManager(prefs, context, GlobalScope)
@Provides
@Singleton
fun provideSharedNavMessageManager(
@ApplicationContext context: Context,
prefs: SharedPreferences,
): SharedNavMessageManager =
SharedNavMessageManager(context, GlobalScope, prefs)
@Provides
@Singleton
fun provideSharedMeasurementsManager(
prefs: SharedPreferences,
@ApplicationContext context: Context
): SharedGnssMeasurementManager =
SharedGnssMeasurementManager(prefs, context, GlobalScope)
@Provides
@Singleton
fun provideSharedAntennaManager(
@ApplicationContext context: Context,
prefs: SharedPreferences,
): SharedAntennaManager =
SharedAntennaManager(context, GlobalScope,prefs)
}

View File

@@ -1,4 +1,4 @@
package com.android.gpstest.io;
package com.android.gpstest.library.io;
import java.io.File;
import java.io.FileFilter;

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008-2021 The Android Open Source Project, Sean J. Barbeau
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<group android:id="@+id/gps_group">
<item
android:id="@+id/filter_sats"
android:title="@string/menu_option_filter"
android:icon="@drawable/ic_baseline_filter_list_24"
android:orderInCategory="2"
app:showAsAction="ifRoom"
android:contentDescription="@string/menu_option_filter_content_description" />
<item
android:id="@+id/share"
android:title="@string/share"
android:icon="@drawable/social_share"
android:orderInCategory="10"
app:showAsAction="ifRoom|withText"
android:contentDescription="@string/share" />
<item
android:id="@+id/gps_switch_item"
android:title="@string/gps_switch"
android:orderInCategory="11"
app:showAsAction="always|withText"
app:actionLayout="@layout/gps_switch"
android:contentDescription="@string/gps_switch_content_description"/>
</group>
</menu>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 Sean J. Barbeau (sjbarbeau@gmail.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<group android:id="@+id/gps_group">
<item
android:id="@+id/sort_sats"
android:title="@string/menu_option_sort_by"
android:icon="@drawable/ic_action_sort"
android:orderInCategory="1"
app:showAsAction="ifRoom"
android:contentDescription="@string/menu_option_sort_by_content_description" />
</group>
</menu>

View File

@@ -1,4 +1,4 @@
package com.android.gpstest.model
package com.android.gpstest.library.model
import kotlin.math.abs

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.model;
package com.android.gpstest.library.model;
/**
* Types of constellations

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.model
package com.android.gpstest.library.model
/**
* Types of coordinates

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.model;
package com.android.gpstest.library.model;
/**
* Container class for DOP values

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.model;
package com.android.gpstest.library.model;
/**
* Types of Global Navigation Satellite Systems

View File

@@ -1,4 +1,4 @@
package com.android.gpstest.model
package com.android.gpstest.library.model
/**
* Model class for holding measured error between two locations

View File

@@ -1,4 +1,4 @@
package com.android.gpstest.model
package com.android.gpstest.library.model
/**
* A container class to hold a NMEA [message] with a system [timestamp] (coming from

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.model
package com.android.gpstest.library.model
/**
* Container class holding rotation sensor timestamp, and [values], where the first index is the

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.model
package com.android.gpstest.library.model
/**
* A container class that represents a satellite sending GNSS or SBAS signals ([status]). The [id] of

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.model
package com.android.gpstest.library.model
/**
* A container class that holds a group of [satellites] from multiple constellations (e.g., GNSS,
@@ -21,5 +21,6 @@ package com.android.gpstest.model
* combination of constellation and svID. The key is created using SatelliteUtils.createGnssSatelliteKey().
*/
data class SatelliteGroup(
val satellites: Map<String, Satellite>,
val satelliteMetadata: SatelliteMetadata)
val satellites: Map<String, Satellite>,
val satelliteMetadata: SatelliteMetadata
)

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.model
package com.android.gpstest.library.model
/**
* A container class that holds metadata and statistics information about a group of satellites.
@@ -32,20 +32,20 @@ package com.android.gpstest.model
* has been detected as having duplicate carrier frequency data with another signal.
*/
data class SatelliteMetadata(
val numSignalsInView: Int = 0,
val numSignalsUsed: Int = 0,
val numSignalsTotal: Int = 0,
val numSatsInView: Int = 0,
val numSatsUsed: Int = 0,
val numSatsTotal: Int = 0,
val supportedGnss: Set<GnssType> = HashSet(),
val supportedGnssCfs: Set<String> = HashSet(),
val supportedSbas: Set<SbasType> = HashSet(),
val supportedSbasCfs: Set<String> = HashSet(),
val unknownCarrierStatuses: Map<String, SatelliteStatus> = HashMap(),
val duplicateCarrierStatuses: Map<String, SatelliteStatus> = HashMap(),
val isDualFrequencyPerSatInView: Boolean = false,
val isDualFrequencyPerSatInUse: Boolean = false,
val isNonPrimaryCarrierFreqInView: Boolean = false,
val isNonPrimaryCarrierFreqInUse: Boolean = false,
val numSignalsInView: Int = 0,
val numSignalsUsed: Int = 0,
val numSignalsTotal: Int = 0,
val numSatsInView: Int = 0,
val numSatsUsed: Int = 0,
val numSatsTotal: Int = 0,
val supportedGnss: Set<GnssType> = HashSet(),
val supportedGnssCfs: Set<String> = HashSet(),
val supportedSbas: Set<SbasType> = HashSet(),
val supportedSbasCfs: Set<String> = HashSet(),
val unknownCarrierStatuses: Map<String, SatelliteStatus> = HashMap(),
val duplicateCarrierStatuses: Map<String, SatelliteStatus> = HashMap(),
val isDualFrequencyPerSatInView: Boolean = false,
val isDualFrequencyPerSatInUse: Boolean = false,
val isNonPrimaryCarrierFreqInView: Boolean = false,
val isNonPrimaryCarrierFreqInUse: Boolean = false,
)

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.model;
package com.android.gpstest.library.model;
/**
* Types of Global Navigation Satellite Systems

View File

@@ -13,21 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.model
package com.android.gpstest.library.model
/**
* Mirrors the GnssStatus class (https://developer.android.com/reference/android/location/GnssStatus),
* but uses internal GnssType and SbasType values for GNSS and SBAS constellations
*/
data class SatelliteStatus (
val svid: Int,
val gnssType: GnssType,
var cn0DbHz: Float,
val hasAlmanac: Boolean,
val hasEphemeris: Boolean,
val usedInFix: Boolean,
var elevationDegrees: Float,
var azimuthDegrees: Float) {
val svid: Int,
val gnssType: GnssType,
var cn0DbHz: Float,
val hasAlmanac: Boolean,
val hasEphemeris: Boolean,
val usedInFix: Boolean,
var elevationDegrees: Float,
var azimuthDegrees: Float) {
var sbasType: SbasType = SbasType.UNKNOWN
var hasCarrierFrequency: Boolean = false
var carrierFrequencyHz: Double = 0.0

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.model;
package com.android.gpstest.library.model;
/**
* Types of Satellite Based Augmentation Systems. See

View File

@@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.ui
package com.android.gpstest.library.ui
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.location.Location
import androidx.annotation.VisibleForTesting
@@ -24,17 +25,17 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.android.gpstest.data.FirstFixState
import com.android.gpstest.data.FixState
import com.android.gpstest.data.LocationRepository
import com.android.gpstest.model.*
import com.android.gpstest.util.CarrierFreqUtils.getCarrierFrequencyLabel
import com.android.gpstest.util.FormatUtils.formatTtff
import com.android.gpstest.util.NmeaUtils
import com.android.gpstest.util.PreferenceUtil
import com.android.gpstest.util.PreferenceUtils
import com.android.gpstest.util.SatelliteUtil.toSatelliteGroup
import com.android.gpstest.util.SatelliteUtil.toSatelliteStatus
import com.android.gpstest.library.data.FirstFixState
import com.android.gpstest.library.data.FixState
import com.android.gpstest.library.data.LocationRepository
import com.android.gpstest.library.model.*
import com.android.gpstest.library.util.CarrierFreqUtils.getCarrierFrequencyLabel
import com.android.gpstest.library.util.FormatUtils.formatTtff
import com.android.gpstest.library.util.NmeaUtils
import com.android.gpstest.library.util.PreferenceUtil
import com.android.gpstest.library.util.PreferenceUtils
import com.android.gpstest.library.util.SatelliteUtil.toSatelliteGroup
import com.android.gpstest.library.util.SatelliteUtil.toSatelliteStatus
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
@@ -50,8 +51,10 @@ import javax.inject.Inject
@ExperimentalCoroutinesApi
@HiltViewModel
class SignalInfoViewModel @Inject constructor(
context: Context,
application: Application,
private val repository: LocationRepository
private val repository: LocationRepository,
prefs: SharedPreferences
) : AndroidViewModel(application) {
//
// Flows from the repository
@@ -115,21 +118,21 @@ class SignalInfoViewModel @Inject constructor(
// Preference listener that will cancel the above flows when the user turns off tracking via UI
private val trackingListener: SharedPreferences.OnSharedPreferenceChangeListener =
PreferenceUtil.newStopTrackingListener { setStarted(false) }
PreferenceUtil.newStopTrackingListener({setStarted(context, false, prefs)}, prefs)
init {
viewModelScope.launch {
observeLocationUpdateStates()
observeGnssStates()
com.android.gpstest.Application.prefs.registerOnSharedPreferenceChangeListener(trackingListener)
observeLocationUpdateStates(context, prefs)
observeGnssStates(prefs)
prefs.registerOnSharedPreferenceChangeListener(trackingListener)
}
}
@ExperimentalCoroutinesApi
private fun observeLocationUpdateStates() {
private fun observeLocationUpdateStates(context: Context, prefs: SharedPreferences) {
repository.receivingLocationUpdates
.onEach {
setStarted(it)
setStarted(context, it, prefs)
}
.launchIn(viewModelScope)
}
@@ -151,7 +154,7 @@ class SignalInfoViewModel @Inject constructor(
}
@ExperimentalCoroutinesApi
private fun observeGnssFlow() {
private fun observeGnssFlow(context: Context, prefs: SharedPreferences) {
if (gnssFlow?.isActive == true) {
// If we're already observing updates, don't register again
return
@@ -161,19 +164,19 @@ class SignalInfoViewModel @Inject constructor(
.map { it.toSatelliteStatus() }
.onEach {
//Log.d(TAG, "SignalInfoViewModel gnssStatus: ${it}")
updateStatus(it)
updateStatus(context, it, prefs)
}
.launchIn(viewModelScope)
}
private fun observeGnssStates() {
private fun observeGnssStates(prefs: SharedPreferences) {
repository.firstFixState
.onEach {
when (it) {
is FirstFixState.Acquired -> {
onGnssFirstFix(it.ttffMillis)
}
is FirstFixState.NotAcquired -> if (PreferenceUtils.isTrackingStarted()) onGnssFixLost()
is FirstFixState.NotAcquired -> if (PreferenceUtils.isTrackingStarted(prefs)) onGnssFixLost()
}
}
.launchIn(viewModelScope)
@@ -181,7 +184,7 @@ class SignalInfoViewModel @Inject constructor(
.onEach {
when (it) {
is FixState.Acquired -> onGnssFixAcquired()
is FixState.NotAcquired -> if (PreferenceUtils.isTrackingStarted()) onGnssFixLost()
is FixState.NotAcquired -> if (PreferenceUtils.isTrackingStarted(prefs)) onGnssFixLost()
}
}
.launchIn(viewModelScope)
@@ -204,12 +207,12 @@ class SignalInfoViewModel @Inject constructor(
@ExperimentalCoroutinesApi
@VisibleForTesting
fun updateStatus(status: List<SatelliteStatus>) {
fun updateStatus(context: Context, status: List<SatelliteStatus>, prefs: SharedPreferences) {
_allStatuses.value = status
_allSatellitesGroup.value = status.toSatelliteGroup()
// Get filter set by user in UI
val filter = PreferenceUtils.gnssFilter()
val filter = PreferenceUtils.gnssFilter(context, prefs)
// Split list into GNSS and SBAS statuses, apply "shown" filter, and update view model
val (gnssStatus, sbasStatus) = status
@@ -221,7 +224,7 @@ class SignalInfoViewModel @Inject constructor(
}
_filteredStatuses.value = gnssStatus + sbasStatus
setFilteredAndSortedStatuses(sort(gnssStatus, true), sort(sbasStatus, false))
setFilteredAndSortedStatuses(sort(context, gnssStatus, true, prefs), sort(context, sbasStatus, false, prefs))
}
/**
@@ -229,8 +232,8 @@ class SignalInfoViewModel @Inject constructor(
* the user, with [isGnss] set to true if the list contains all GNSS signals and false if
* it contains all SBAS signals
*/
private fun sort(status: List<SatelliteStatus>, isGnss: Boolean): List<SatelliteStatus> {
return when (PreferenceUtils.getSatSortOrderFromPreferences()) {
private fun sort(context: Context, status: List<SatelliteStatus>, isGnss: Boolean, prefs: SharedPreferences): List<SatelliteStatus> {
return when (PreferenceUtils.getSatSortOrderFromPreferences(context, prefs)) {
0 -> {
// Sort by Constellation
if (isGnss) {
@@ -387,7 +390,7 @@ class SignalInfoViewModel @Inject constructor(
@ExperimentalCoroutinesApi
@SuppressLint("NotifyDataSetChanged")
private fun setStarted(started: Boolean) {
private fun setStarted(context: Context, started: Boolean, prefs: SharedPreferences) {
if (started == this.started) {
// State hasn't changed - no op and return
return
@@ -395,7 +398,7 @@ class SignalInfoViewModel @Inject constructor(
if (started) {
// Activity or service is observing updates, so observe here too
observeLocationFlow()
observeGnssFlow()
observeGnssFlow(context, prefs)
observeNmeaFlow()
} else {
// Cancel updates (Note that these are canceled via trackingListener preference listener
@@ -527,7 +530,7 @@ class SignalInfoViewModel @Inject constructor(
_dop.value = DilutionOfPrecision(Double.NaN, Double.NaN, Double.NaN)
_filteredSatelliteMetadata.value = SatelliteMetadata()
_fixState.value = FixState.NotAcquired
_allSatellitesGroup.value = SatelliteGroup(emptyMap(),SatelliteMetadata())
_allSatellitesGroup.value = SatelliteGroup(emptyMap(), SatelliteMetadata())
gotFirstFix = false
}

View File

@@ -1,8 +1,8 @@
package com.android.gpstest.util
package com.android.gpstest.library.util
import android.location.Location
import com.android.gpstest.model.MeasuredError
import com.android.gpstest.library.model.MeasuredError
/**
* Utilities for comparing two locations to measure error

View File

@@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.util;
package com.android.gpstest.library.util;
import android.location.GnssAntennaInfo;
import android.os.Build;
import androidx.annotation.RequiresApi;
import com.android.gpstest.model.SatelliteStatus;
import com.android.gpstest.library.model.SatelliteStatus;
public class CarrierFreqUtils {

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.gpstest.util
package com.android.gpstest.library.util
import android.os.Build
import androidx.annotation.VisibleForTesting
@@ -44,7 +44,7 @@ class DateTimeUtils {
}
@VisibleForTesting
internal fun isTimeValidLegacy(time: Long): Boolean {
fun isTimeValidLegacy(time: Long): Boolean {
return TimeUnit.MILLISECONDS.toDays(abs(System.currentTimeMillis() - time)) < NUM_DAYS_TIME_VALID
}
}

View File

@@ -1,69 +1,70 @@
package com.android.gpstest.util
package com.android.gpstest.library.util
import android.content.Context
import android.content.SharedPreferences
import android.location.GnssAntennaInfo
import android.location.GnssClock
import android.location.GnssMeasurement
import android.location.Location
import android.os.Build
import androidx.annotation.RequiresApi
import com.android.gpstest.Application.Companion.app
import com.android.gpstest.R
import com.android.gpstest.model.CoordinateType
import com.android.gpstest.model.Orientation
import com.android.gpstest.model.SatelliteGroup
import com.android.gpstest.model.SatelliteStatus
import com.android.gpstest.util.SatelliteUtil.isBearingAccuracySupported
import com.android.gpstest.util.SatelliteUtil.isSpeedAccuracySupported
import com.android.gpstest.util.SatelliteUtil.isVerticalAccuracySupported
import com.android.gpstest.util.SatelliteUtil.toGnssStatusConstellationType
import com.android.gpstest.library.R
import com.android.gpstest.library.model.CoordinateType
import com.android.gpstest.library.model.Orientation
import com.android.gpstest.library.model.SatelliteGroup
import com.android.gpstest.library.model.SatelliteStatus
import com.android.gpstest.library.util.SatelliteUtil.isBearingAccuracySupported
import com.android.gpstest.library.util.SatelliteUtil.isSpeedAccuracySupported
import com.android.gpstest.library.util.SatelliteUtil.isVerticalAccuracySupported
import com.android.gpstest.library.util.SatelliteUtil.toGnssStatusConstellationType
import java.util.concurrent.TimeUnit
/**
* Provides access to formatting utilities for view in the UI
**/
internal object FormatUtils {
fun formatLatOrLon(latOrLong: Double, coordinateType: CoordinateType): String {
object FormatUtils {
fun formatLatOrLon(context: Context, latOrLong: Double, coordinateType: CoordinateType, prefs: SharedPreferences): String {
if (latOrLong == 0.0) return " "
when (PreferenceUtil.coordinateFormat()) {
when (PreferenceUtil.coordinateFormat(context, prefs)) {
"dd" -> {
// Decimal degrees
return app.getString(R.string.lat_or_lon, latOrLong)
return context.getString(R.string.lat_or_lon, latOrLong)
}
"dms" -> {
// Degrees minutes seconds
return UIUtils.getDMSFromLocation(
app,
return LibUIUtils.getDMSFromLocation(
context,
latOrLong,
coordinateType
)
}
"ddm" -> {
// Degrees decimal minutes
return UIUtils.getDDMFromLocation(
app,
return LibUIUtils.getDDMFromLocation(
context,
latOrLong,
coordinateType
)
}
else -> {
// Decimal degrees
return app.getString(R.string.lat_or_lon, latOrLong)
return context.getString(R.string.lat_or_lon, latOrLong)
}
}
}
fun formatAltitude(location: Location): String {
fun formatAltitude(context: Context, location: Location, prefs: SharedPreferences): String {
if (location.hasAltitude()) {
val text = when {
PreferenceUtil.distanceUnits().equals(PreferenceUtil.METERS, ignoreCase = true) -> {
app.getString(R.string.gps_altitude_value_meters, location.altitude)
PreferenceUtil.distanceUnits(context, prefs).equals(PreferenceUtil.METERS, ignoreCase = true) -> {
context.getString(R.string.gps_altitude_value_meters, location.altitude)
}
else -> {
// Feet
app.getString(
context.getString(
R.string.gps_altitude_value_feet,
UIUtils.toFeet(location.altitude)
LibUIUtils.toFeet(location.altitude)
)
}
}
@@ -73,25 +74,25 @@ internal object FormatUtils {
}
}
fun formatSpeed(location: Location): String {
fun formatSpeed(context: Context, location: Location, prefs: SharedPreferences): String {
if (location.hasSpeed()) {
val text = when {
PreferenceUtil.speedUnits()
PreferenceUtil.speedUnits(context, prefs)
.equals(PreferenceUtil.METERS_PER_SECOND, ignoreCase = true) -> {
app.getString(R.string.gps_speed_value_meters_sec, location.speed)
context.getString(R.string.gps_speed_value_meters_sec, location.speed)
}
PreferenceUtil.speedUnits()
PreferenceUtil.speedUnits(context, prefs)
.equals(PreferenceUtil.KILOMETERS_PER_HOUR, ignoreCase = true) -> {
app.getString(
context.getString(
R.string.gps_speed_value_kilometers_hour,
UIUtils.toKilometersPerHour(location.speed)
LibUIUtils.toKilometersPerHour(location.speed)
)
}
else -> {
// Miles per hour
app.getString(
context.getString(
R.string.gps_speed_value_miles_hour,
UIUtils.toMilesPerHour(location.speed)
LibUIUtils.toMilesPerHour(location.speed)
)
}
}
@@ -115,36 +116,36 @@ internal object FormatUtils {
}
}
fun formatAccuracy(location: Location): String {
fun formatAccuracy(context: Context, location: Location, prefs: SharedPreferences): String {
if (location.isVerticalAccuracySupported()) {
if (PreferenceUtil.distanceUnits().equals(PreferenceUtil.METERS, ignoreCase = true)) {
return app.getString(
if (PreferenceUtil.distanceUnits(context, prefs).equals(PreferenceUtil.METERS, ignoreCase = true)) {
return context.getString(
R.string.gps_hor_and_vert_accuracy_value_meters,
location.accuracy,
location.verticalAccuracyMeters
)
} else {
// Feet
return app.getString(
return context.getString(
R.string.gps_hor_and_vert_accuracy_value_feet,
UIUtils.toFeet(location.accuracy.toDouble()),
UIUtils.toFeet(
LibUIUtils.toFeet(location.accuracy.toDouble()),
LibUIUtils.toFeet(
location.verticalAccuracyMeters.toDouble()
)
)
}
} else {
if (location.hasAccuracy()) {
return if (PreferenceUtil.distanceUnits()
return if (PreferenceUtil.distanceUnits(context, prefs)
.equals(PreferenceUtil.METERS, ignoreCase = true)) {
app.getString(
context.getString(
R.string.gps_accuracy_value_meters, location.accuracy
)
} else {
// Feet
app.getString(
context.getString(
R.string.gps_accuracy_value_feet,
UIUtils.toFeet(location.accuracy.toDouble())
LibUIUtils.toFeet(location.accuracy.toDouble())
)
}
}
@@ -152,44 +153,44 @@ internal object FormatUtils {
return ""
}
fun formatAltitudeMsl(altitudeMsl: Double): String {
fun formatAltitudeMsl(context: Context, altitudeMsl: Double, prefs: SharedPreferences): String {
if (altitudeMsl.isNaN()) return ""
return if (PreferenceUtil.distanceUnits()
return if (PreferenceUtil.distanceUnits(context, prefs)
.equals(PreferenceUtil.METERS, ignoreCase = true)) {
app.getString(
context.getString(
R.string.gps_altitude_msl_value_meters,
altitudeMsl)
} else {
app.getString(
context.getString(
R.string.gps_altitude_msl_value_feet,
UIUtils.toFeet(altitudeMsl)
LibUIUtils.toFeet(altitudeMsl)
)
}
}
fun formatSpeedAccuracy(location: Location): String {
fun formatSpeedAccuracy(context: Context, location: Location, prefs: SharedPreferences): String {
if (location.isSpeedAccuracySupported()) {
when {
PreferenceUtil.speedUnits()
PreferenceUtil.speedUnits(context, prefs)
.equals(PreferenceUtil.METERS_PER_SECOND, ignoreCase = true) -> {
return app.getString(
return context.getString(
R.string.gps_speed_acc_value_meters_sec,
location.speedAccuracyMetersPerSecond
)
}
PreferenceUtil.speedUnits()
PreferenceUtil.speedUnits(context, prefs)
.equals(PreferenceUtil.KILOMETERS_PER_HOUR, ignoreCase = true) -> {
return app.getString(
return context.getString(
R.string.gps_speed_acc_value_km_hour,
UIUtils.toKilometersPerHour(location.speedAccuracyMetersPerSecond)
LibUIUtils.toKilometersPerHour(location.speedAccuracyMetersPerSecond)
)
}
else -> {
// Miles per hour
return app.getString(
return context.getString(
R.string.gps_speed_acc_value_miles_hour,
UIUtils.toMilesPerHour(location.speedAccuracyMetersPerSecond)
LibUIUtils.toMilesPerHour(location.speedAccuracyMetersPerSecond)
)
}
}
@@ -197,13 +198,13 @@ internal object FormatUtils {
return ""
}
fun formatBearing(location: Location): String {
return app.getString(R.string.gps_bearing_value, location.bearing)
fun formatBearing(context: Context, location: Location): String {
return context.getString(R.string.gps_bearing_value, location.bearing)
}
fun formatBearingAccuracy(location: Location): String {
fun formatBearingAccuracy(context: Context, location: Location): String {
return if (location.isBearingAccuracySupported()) {
app.getString(
context.getString(
R.string.gps_bearing_acc_value,
location.bearingAccuracyDegrees
)
@@ -216,9 +217,9 @@ internal object FormatUtils {
* Returns metadata about the satellite group formatted for a notification title, like
* "1/3 sats | 2/4 signals (E1, E5a, L1)"
*/
fun SatelliteGroup.toNotificationTitle(): String {
fun SatelliteGroup.toNotificationTitle(context: Context): String {
val meta = this.satelliteMetadata
return app.getString(
return context.getString(
R.string.notification_title,
meta.numSatsUsed,
meta.numSatsInView,
@@ -333,14 +334,14 @@ internal object FormatUtils {
"${phaseCenterOffset.xOffsetUncertaintyMm.toLog()},${phaseCenterOffset.yOffsetMm.toLog()}," +
"${phaseCenterOffset.yOffsetUncertaintyMm.toLog()},${phaseCenterOffset.zOffsetMm.toLog()}," +
"${phaseCenterOffset.zOffsetUncertaintyMm.toLog()},${
IOUtils.serialize(
phaseCenterVariationCorrections!!.correctionsArray
)
IOUtils.serialize(
phaseCenterVariationCorrections!!.correctionsArray
)
},${IOUtils.serialize(phaseCenterVariationCorrections!!.correctionUncertaintiesArray)}," +
"${phaseCenterVariationCorrections!!.deltaPhi.toLog()},${phaseCenterVariationCorrections!!.deltaTheta.toLog()},${
IOUtils.serialize(
signalGainCorrections!!.correctionsArray
)
IOUtils.serialize(
signalGainCorrections!!.correctionsArray
)
},${IOUtils.serialize(signalGainCorrections!!.correctionUncertaintiesArray)}," +
"${signalGainCorrections!!.deltaPhi.toLog()},${signalGainCorrections!!.deltaTheta.toLog()}"
}

View File

@@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.util;
package com.android.gpstest.library.util;
import static android.content.Intent.ACTION_VIEW;
import static com.android.gpstest.util.LocationUtils.isValidLatitude;
import static com.android.gpstest.util.LocationUtils.isValidLongitude;
import static com.android.gpstest.library.util.LocationUtils.isValidLatitude;
import static com.android.gpstest.library.util.LocationUtils.isValidLongitude;
import android.app.Activity;
import android.content.ClipData;
@@ -37,11 +37,10 @@ import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;
import com.android.gpstest.Application;
import com.android.gpstest.BuildConfig;
import com.android.gpstest.R;
import com.android.gpstest.io.FileToDeleteFilter;
import com.android.gpstest.library.R;
import com.android.gpstest.library.io.FileToDeleteFilter;
import com.google.zxing.integration.android.IntentIntegrator;
import com.sothree.slidinguppanel.library.BuildConfig;
import java.io.File;
import java.io.FileFilter;
@@ -72,20 +71,20 @@ public class IOUtils {
* SHOW_RADAR action (com.google.android.radar.SHOW_RADAR) with a valid latitude and longitude or ACTION_VIEW action with geo URI, or
* null if the Intent doesn't have a SHOW_RADAR action or the intent has an invalid latitude or longitude
*/
public static Location getLocationFromIntent(Intent intent) {
public static Location getLocationFromIntent(Context context, Intent intent) {
Location groundTruth = null;
if (isShowRadarIntent(intent)) {
if (isShowRadarIntent(context, intent)) {
double lat = Double.NaN, lon = Double.NaN;
float latFloat = intent.getFloatExtra(Application.Companion.getApp().getString(R.string.radar_lat_key), Float.NaN);
float lonFloat = intent.getFloatExtra(Application.Companion.getApp().getString(R.string.radar_lon_key), Float.NaN);
float latFloat = intent.getFloatExtra(context.getString(R.string.radar_lat_key), Float.NaN);
float lonFloat = intent.getFloatExtra(context.getString(R.string.radar_lon_key), Float.NaN);
if (isValidLatitude(latFloat) && isValidLongitude(lonFloat)) {
// Use the float values
lat = (double) latFloat;
lon = (double) lonFloat;
} else {
// Try parsing doubles
double latDouble = intent.getDoubleExtra(Application.Companion.getApp().getString(R.string.radar_lat_key), Double.NaN);
double lonDouble = intent.getDoubleExtra(Application.Companion.getApp().getString(R.string.radar_lon_key), Double.NaN);
double latDouble = intent.getDoubleExtra(context.getString(R.string.radar_lat_key), Double.NaN);
double lonDouble = intent.getDoubleExtra(context.getString(R.string.radar_lon_key), Double.NaN);
if (isValidLatitude(latDouble) && isValidLongitude(lonDouble)) {
lat = latDouble;
lon = lonDouble;
@@ -96,21 +95,21 @@ public class IOUtils {
groundTruth = new Location("ground_truth");
groundTruth.setLatitude(lat);
groundTruth.setLongitude(lon);
if (intent.hasExtra(Application.Companion.getApp().getString(R.string.radar_alt_key))) {
float altitude = intent.getFloatExtra(Application.Companion.getApp().getString(R.string.radar_alt_key), Float.NaN);
if (intent.hasExtra(context.getString(R.string.radar_alt_key))) {
float altitude = intent.getFloatExtra(context.getString(R.string.radar_alt_key), Float.NaN);
if (!Float.isNaN(altitude)) {
groundTruth.setAltitude(altitude);
} else {
// Try the double version
double altitudeDouble = intent.getDoubleExtra(Application.Companion.getApp().getString(R.string.radar_alt_key), Double.NaN);
double altitudeDouble = intent.getDoubleExtra(context.getString(R.string.radar_alt_key), Double.NaN);
if (!Double.isNaN(altitudeDouble)) {
groundTruth.setAltitude(altitudeDouble);
}
}
}
}
} else if (isGeoIntent(intent)) {
groundTruth = getLocationFromGeoUri(intent.getData().toString());
} else if (isGeoIntent(context, intent)) {
groundTruth = getLocationFromGeoUri(context, intent.getData().toString());
}
return groundTruth;
}
@@ -121,10 +120,10 @@ public class IOUtils {
* @param intent
* @return true if the provided intent has the SHOW_RADAR action (com.google.android.radar.SHOW_RADAR), or false if it does not
*/
public static boolean isShowRadarIntent(Intent intent) {
public static boolean isShowRadarIntent(Context context, Intent intent) {
return intent != null &&
intent.getAction() != null &&
intent.getAction().equals(Application.Companion.getApp().getString(R.string.show_radar_intent));
intent.getAction().equals(context.getString(R.string.show_radar_intent));
}
/**
@@ -133,12 +132,12 @@ public class IOUtils {
* @param intent
* @return true if the provided intent has the ACTION_VIEW action and contains a geo: URI, or false if it does not
*/
public static boolean isGeoIntent(Intent intent) {
public static boolean isGeoIntent(Context context, Intent intent) {
return intent != null &&
intent.getAction() != null &&
intent.getAction().equals(ACTION_VIEW) &&
intent.getData() != null &&
intent.getData().toString().startsWith(Application.Companion.getApp().getString(R.string.geo_uri_prefix));
intent.getData().toString().startsWith(context.getString(R.string.geo_uri_prefix));
}
/**
@@ -147,8 +146,8 @@ public class IOUtils {
* @param location location information to be added to the intent
* @return a SHOW_RADAR intent with the provided latitude, longitude, and, if provided, altitude, all in WGS-84
*/
public static Intent createShowRadarIntent(Location location) {
return createShowRadarIntent(location.getLatitude(), location.getLongitude(), location.hasAltitude() ? location.getAltitude() : null);
public static Intent createShowRadarIntent(Context context, Location location) {
return createShowRadarIntent(context, location.getLatitude(), location.getLongitude(), location.hasAltitude() ? location.getAltitude() : null);
}
/**
@@ -159,12 +158,12 @@ public class IOUtils {
* @param alt altitude in meters above WGS84 ellipsoid, or null if altitude shouldn't be included
* @return a SHOW_RADAR intent with the provided latitude, longitude, and, if provided, altitude, all in WGS-84
*/
public static Intent createShowRadarIntent(double lat, double lon, Double alt) {
Intent intent = new Intent(Application.Companion.getApp().getString(R.string.show_radar_intent));
intent.putExtra(Application.Companion.getApp().getString(R.string.radar_lat_key), lat);
intent.putExtra(Application.Companion.getApp().getString(R.string.radar_lon_key), lon);
public static Intent createShowRadarIntent(Context context, double lat, double lon, Double alt) {
Intent intent = new Intent(context.getString(R.string.show_radar_intent));
intent.putExtra(context.getString(R.string.radar_lat_key), lat);
intent.putExtra(context.getString(R.string.radar_lon_key), lon);
if (alt != null && !Double.isNaN(alt)) {
intent.putExtra(Application.Companion.getApp().getString(R.string.radar_alt_key), alt);
intent.putExtra(context.getString(R.string.radar_alt_key), alt);
}
return intent;
}
@@ -181,8 +180,8 @@ public class IOUtils {
* @param geoUri a Geo URI following RFC 5870 (e.g., geo:37.786971,-122.399677)
* @return a location from the provided Geo URI (RFC 5870) or null if one can't be parsed
*/
public static Location getLocationFromGeoUri(String geoUri) {
if (TextUtils.isEmpty(geoUri) || !geoUri.startsWith(Application.Companion.getApp().getString(R.string.geo_uri_prefix))) {
public static Location getLocationFromGeoUri(Context context, String geoUri) {
if (TextUtils.isEmpty(geoUri) || !geoUri.startsWith(context.getString(R.string.geo_uri_prefix))) {
return null;
}
Location l = null;
@@ -210,11 +209,11 @@ public class IOUtils {
* @param includeAltitude true if altitude should be included in the Geo URI, false if it should be omitted. If the location doesn't have an altitude value this parameter has no effect.
* @return a Geo URI (RFC 5870) from the provided location, or null if one can't be created
*/
public static String createGeoUri(Location location, boolean includeAltitude) {
public static String createGeoUri(Context context, Location location, boolean includeAltitude) {
if (location == null) {
return null;
}
String geoUri = Application.Companion.getApp().getString(R.string.geo_uri_prefix);
String geoUri = context.getString(R.string.geo_uri_prefix);
geoUri += location.getLatitude() + ",";
geoUri += location.getLongitude();
if (location.hasAltitude() && includeAltitude) {
@@ -228,9 +227,9 @@ public class IOUtils {
*
* @param location the location string to copy to the clipboard
*/
public static void copyToClipboard(String location) {
ClipboardManager clipboard = (ClipboardManager) Application.Companion.getApp().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(Application.Companion.getApp().getString(R.string.pref_file_location_output_title), location);
public static void copyToClipboard(Context context, String location) {
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(context.getString(R.string.pref_file_location_output_title), location);
clipboard.setPrimaryClip(clip);
}
@@ -274,7 +273,7 @@ public class IOUtils {
* @param activity Activity used to open the chooser to send the file
* @param files files to send
*/
public static void sendLogFile(Activity activity, File... files) {
public static void sendLogFile(Context context, Activity activity, File... files) {
ArrayList<android.net.Uri> uris = new ArrayList<>();
for (File file : files) {
if (file != null) {
@@ -282,7 +281,7 @@ public class IOUtils {
}
}
IOUtils.sendLogFile(activity, uris);
IOUtils.sendLogFile(context, activity, uris);
}
/**
@@ -291,14 +290,14 @@ public class IOUtils {
* @param activity
* @param fileUris Android URIs for the File to be attached
*/
public static void sendLogFile(Activity activity, ArrayList<android.net.Uri> fileUris) {
public static void sendLogFile(Context context, Activity activity, ArrayList<android.net.Uri> fileUris) {
Intent emailIntent = new Intent(Intent.ACTION_SEND_MULTIPLE);
emailIntent.setType("*/*");
emailIntent.putExtra(Intent.EXTRA_SUBJECT, "GnssLog from GPSTest");
emailIntent.putExtra(Intent.EXTRA_TEXT, "");
Log.d(TAG, "Sending " + fileUris);
emailIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, fileUris);
activity.startActivity(Intent.createChooser(emailIntent, Application.Companion.getApp().getString(R.string.send_log)));
activity.startActivity(Intent.createChooser(emailIntent, context.getString(R.string.send_log)));
}
/**
@@ -371,9 +370,9 @@ public class IOUtils {
*
* @return the GNSS hardware year for the device, or null if the year couldn't be determined
*/
public static String getGnssHardwareYear() {
public static String getGnssHardwareYear(Context context) {
String year = "";
LocationManager locationManager = (LocationManager) Application.Companion.getApp().getSystemService(Context.LOCATION_SERVICE);
LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
year = String.valueOf(locationManager.getGnssYearOfHardware());
@@ -403,9 +402,9 @@ public class IOUtils {
*
* @return the GNSS hardware model name for the device, or empty String if the year couldn't be determined
*/
public static String getGnssHardwareModelName() {
public static String getGnssHardwareModelName(Context context) {
String modelName = "";
LocationManager locationManager = (LocationManager) Application.Companion.getApp().getSystemService(Context.LOCATION_SERVICE);
LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (locationManager.getGnssHardwareModelName() != null) {
@@ -420,11 +419,11 @@ public class IOUtils {
* @param locationManager
* @return true if the command succeeded, false if it failed
*/
public static boolean forceTimeInjection(LocationManager locationManager) {
public static boolean forceTimeInjection(Context context, LocationManager locationManager) {
if (locationManager == null) {
return false;
}
return locationManager.sendExtraCommand(LocationManager.GPS_PROVIDER, Application.Companion.getApp().getString(R.string.force_time_injection_command), null);
return locationManager.sendExtraCommand(LocationManager.GPS_PROVIDER, context.getString(R.string.force_time_injection_command), null);
}
/**
@@ -432,15 +431,15 @@ public class IOUtils {
* @param locationManager
* @return true if the command succeeded, false if it failed
*/
public static boolean forcePsdsInjection(LocationManager locationManager) {
public static boolean forcePsdsInjection(Context context, LocationManager locationManager) {
if (locationManager == null) {
return false;
}
String command;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
command = Application.Companion.getApp().getString(R.string.force_psds_injection_command);
command = context.getString(R.string.force_psds_injection_command);
} else {
command = Application.Companion.getApp().getString(R.string.force_xtra_injection_command);
command = context.getString(R.string.force_xtra_injection_command);
}
return locationManager.sendExtraCommand(LocationManager.GPS_PROVIDER, command, null);
}
@@ -450,11 +449,11 @@ public class IOUtils {
* @param locationManager
* @return true if the command succeeded, false if it failed
*/
public static boolean deleteAidingData(LocationManager locationManager) {
public static boolean deleteAidingData(Context context,LocationManager locationManager) {
if (locationManager == null) {
return false;
}
return locationManager.sendExtraCommand(LocationManager.GPS_PROVIDER, Application.Companion.getApp().getString(R.string.delete_aiding_data_command), null);
return locationManager.sendExtraCommand(LocationManager.GPS_PROVIDER, context.getString(R.string.delete_aiding_data_command), null);
}
/**

View File

@@ -0,0 +1,907 @@
/*
* Copyright (C) 2015-2018 University of South Florida, Sean J. Barbeau
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.library.util
import android.Manifest
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.location.Location
import android.location.LocationManager
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.text.Spannable
import android.text.TextUtils
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment
// import com.android.gpstest.library.BuildConfig
import com.android.gpstest.library.R
import com.android.gpstest.library.model.CoordinateType
import com.android.gpstest.library.model.GnssType
import com.android.gpstest.library.model.SbasType
import com.android.gpstest.library.ui.SignalInfoViewModel
import com.google.android.material.chip.Chip
import com.sothree.slidinguppanel.library.BuildConfig
import java.math.BigDecimal
import java.math.RoundingMode
import java.util.*
/**
* Utilities for processing user inteface elements
*/
object LibUIUtils {
const val TAG = "UIUtils"
var PICKFILE_REQUEST_CODE = 101
const val ANIMATION_DURATION_SHORT_MS = 200
const val ANIMATION_DURATION_MEDIUM_MS = 400
const val ANIMATION_DURATION_LONG_MS = 500
const val MIN_VALUE_CN0 = 10.0f
const val MAX_VALUE_CN0 = 45.0f
// Dialogs
const val WHATSNEW_DIALOG = 1
const val HELP_DIALOG = 2
const val CLEAR_ASSIST_WARNING_DIALOG = 3
private const val WHATS_NEW_VER = "whatsNewVer"
/**
* Formats a view so it is ignored for accessible access
*/
@JvmStatic
fun setAccessibilityIgnore(view: View) {
view.isClickable = false
view.isFocusable = false
view.contentDescription = ""
view.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
}
/**
* Converts screen dimension units from dp to pixels, based on algorithm defined in
* http://developer.android.com/guide/practices/screens_support.html#dips-pels
*
* @param dp value in dp
* @return value in pixels
*/
@JvmStatic
fun dpToPixels(context: Context, dp: Float): Int {
// Get the screen's density scale
val scale = context.resources.displayMetrics.density
// Convert the dps to pixels, based on density scale
return (dp * scale + 0.5f).toInt()
}
/**
* Returns true if the current display is wide enough to show the GPS date on the Status screen,
* false if the current display is too narrow to fit the GPS date
* @param context
* @return true if the current display is wide enough to show the GPS date on the Status screen,
* false if the current display is too narrow to fit the GPS date
*/
fun isWideEnoughForDate(context: Context): Boolean {
// 450dp is a little larger than the width of a Samsung Galaxy S8+
val WIDTH_THRESHOLD = dpToPixels(context, 450f)
return context.resources.displayMetrics.widthPixels > WIDTH_THRESHOLD
}
/**
* Returns true if the activity is still active and dialogs can be managed (i.e., displayed
* or dismissed), or false if it is not
*
* @param activity Activity to check for displaying/dismissing a dialog
* @return true if the activity is still active and dialogs can be managed, or false if it is
* not
*/
fun canManageDialog(activity: Activity?): Boolean {
return if (activity == null) {
false
} else !activity.isFinishing && !activity.isDestroyed
}
/**
* Returns true if the fragment is attached to the activity, or false if it is not attached
*
* @param f fragment to be tested
* @return true if the fragment is attached to the activity, or false if it is not attached
*/
fun isFragmentAttached(f: Fragment): Boolean {
return f.activity != null && f.isAdded
}
/**
* Converts the provided C/N0 values to a left margin value (dp) for the avg C/N0 indicator ImageViews in gps_sky_signal
* Left margin range for the C/N0 indicator ImageViews in gps_sky_signal is determined by dimens.xml
* cn0_meter_width (based on device screen width) and cn0_indicator_min_left_margin values.
*
* This is effectively an affine transform - https://math.stackexchange.com/a/377174/554287.
*
* @param cn0 carrier-to-noise density at the antenna of the satellite in dB-Hz (from GnssStatus)
* @return left margin value in dp for the C/N0 indicator ImageViews
*/
@JvmStatic
fun cn0ToIndicatorLeftMarginPx(
cn0: Float,
minIndicatorMarginPx: Int,
maxIndicatorMarginPx: Int
): Int {
return MathUtils.mapToRange(
cn0,
MIN_VALUE_CN0,
MAX_VALUE_CN0,
minIndicatorMarginPx.toFloat(),
maxIndicatorMarginPx.toFloat()
).toInt()
}
/**
* Converts the provided C/N0 values to a left margin value (dp) for the avg C/N0 TextViews in gps_sky_signal
* Left margin range for the C/N0 indicator TextView in gps_sky_signal is determined by dimens.xml
* cn0_meter_width (based on device screen width) and cn0_textview_min_left_margin values.
*
* This is effectively an affine transform - https://math.stackexchange.com/a/377174/554287.
*
* @param cn0 carrier-to-noise density at the antenna of the satellite in dB-Hz (from GnssStatus)
* @return left margin value in dp for the C/N0 TextViews
*/
@JvmStatic
fun cn0ToTextViewLeftMarginPx(
cn0: Float,
minTextViewMarginPx: Int,
maxTextViewMarginPx: Int
): Int {
return MathUtils.mapToRange(
cn0,
MIN_VALUE_CN0,
MAX_VALUE_CN0,
minTextViewMarginPx.toFloat(),
maxTextViewMarginPx.toFloat()
).toInt()
}
/**
* Sets the margins for a given view
*
* @param v View to set the margin for
* @param l left margin, in pixels
* @param t top margin, in pixels
* @param r right margin, in pixels
* @param b bottom margin, in pixels
*/
fun setMargins(v: View, l: Int, t: Int, r: Int, b: Int) {
val p = v.layoutParams as MarginLayoutParams
p.setMargins(l, t, r, b)
v.layoutParams = p
}
/**
* Opens email apps based on the given email address
* @param email address
* @param location string that shows the current location
* @param signalInfoViewModel view model that contains state of GNSS
*/
fun sendEmail(
context: Context,
email: String,
location: String?,
signalInfoViewModel: SignalInfoViewModel,
playServicesVersion: String,
prefs: SharedPreferences
) {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val pm = context.packageManager
val appInfo: PackageInfo
val body = StringBuilder()
body.append(context.getString(R.string.feedback_body))
var versionName: String? = ""
var versionCode = 0
try {
appInfo = pm.getPackageInfo(
context.packageName,
PackageManager.GET_META_DATA
)
versionName = appInfo.versionName
versionCode = appInfo.versionCode
} catch (e: PackageManager.NameNotFoundException) {
// Leave version as empty string
}
// App version
body.append("App version: v")
.append(versionName)
.append(" (")
.append(versionCode)
.append(
"""
-${BuildConfig.FLAVOR})
""".trimIndent()
)
// Device properties
body.append(
"""
Model: ${Build.MODEL}
""".trimIndent()
)
body.append(
"""Android version: ${Build.VERSION.RELEASE} / ${Build.VERSION.SDK_INT}
"""
)
if (!TextUtils.isEmpty(location)) {
body.append("Location: $location\n")
}
body.append(
"""
GNSS HW year: ${IOUtils.getGnssHardwareYear(context)}
""".trimIndent()
)
if (!IOUtils.getGnssHardwareModelName(context).trim { it <= ' ' }.isEmpty()) {
body.append(
"""
GNSS HW name: ${IOUtils.getGnssHardwareModelName(context)}
""".trimIndent()
)
}
// Raw GNSS measurement capability
var capability = prefs.getInt(
context.getString(R.string.capability_key_raw_measurements),
PreferenceUtils.CAPABILITY_UNKNOWN
)
if (capability != PreferenceUtils.CAPABILITY_UNKNOWN) {
body.append(
context.getString(
R.string.capability_title_raw_measurements,
PreferenceUtils.getCapabilityDescription(context, capability)
)
)
}
// Navigation messages capability
capability = prefs.getInt(
context.getString(R.string.capability_key_nav_messages),
PreferenceUtils.CAPABILITY_UNKNOWN
)
if (capability != PreferenceUtils.CAPABILITY_UNKNOWN) {
body.append(
context.getString(
R.string.capability_title_nav_messages,
PreferenceUtils.getCapabilityDescription(context, capability)
)
)
}
// NMEA capability
capability = prefs.getInt(
context.getString(R.string.capability_key_nmea),
PreferenceUtils.CAPABILITY_UNKNOWN
)
if (capability != PreferenceUtils.CAPABILITY_UNKNOWN) {
body.append(
context.getString(
R.string.capability_title_nmea,
PreferenceUtils.getCapabilityDescription(context, capability)
)
)
}
// Inject PSDS capability
capability = prefs.getInt(
context.getString(R.string.capability_key_inject_psds),
PreferenceUtils.CAPABILITY_UNKNOWN
)
if (capability != PreferenceUtils.CAPABILITY_UNKNOWN) {
body.append(
context.getString(
R.string.capability_title_inject_psds,
PreferenceUtils.getCapabilityDescription(context, capability)
)
)
}
// Inject time capability
capability = prefs.getInt(
context.getString(R.string.capability_key_inject_time),
PreferenceUtils.CAPABILITY_UNKNOWN
)
if (capability != PreferenceUtils.CAPABILITY_UNKNOWN) {
body.append(
context.getString(
R.string.capability_title_inject_time,
PreferenceUtils.getCapabilityDescription(context, capability)
)
)
}
// Delete assist capability
capability = prefs.getInt(
context.getString(R.string.capability_key_delete_assist),
PreferenceUtils.CAPABILITY_UNKNOWN
)
if (capability != PreferenceUtils.CAPABILITY_UNKNOWN) {
body.append(
context.getString(
R.string.capability_title_delete_assist,
PreferenceUtils.getCapabilityDescription(context, capability)
)
)
}
// Got fix
body.append(
context.getString(
R.string.capability_title_got_fix,
location != null && signalInfoViewModel.gotFirstFix()
)
)
// We need a fix to determine these attributes reliably
if (location != null && signalInfoViewModel.gotFirstFix()) {
// Dual frequency
body.append(
context.getString(
R.string.capability_title_dual_frequency,
PreferenceUtils.getCapabilityDescription(context, signalInfoViewModel.isNonPrimaryCarrierFreqInView)
)
)
// Supported GNSS
val gnss: List<GnssType> = ArrayList(signalInfoViewModel.getSupportedGnss())
Collections.sort(gnss)
body.append(
context.getString(
R.string.capability_title_supported_gnss, IOUtils.trimEnds(
IOUtils.replaceNavstar(gnss.toString())
)
)
)
// GNSS CF
val gnssCfs: List<String> = ArrayList(signalInfoViewModel.getSupportedGnssCfs())
if (!gnssCfs.isEmpty()) {
Collections.sort(gnssCfs)
body.append(
context.getString(
R.string.capability_title_gnss_cf,
IOUtils.trimEnds(gnssCfs.toString())
)
)
}
// Supported SBAS
val sbas: List<SbasType> = ArrayList(signalInfoViewModel.getSupportedSbas())
if (!sbas.isEmpty()) {
Collections.sort(sbas)
body.append(
context.getString(
R.string.capability_title_supported_sbas,
IOUtils.trimEnds(sbas.toString())
)
)
}
// SBAS CF
val sbasCfs: List<String> = ArrayList(signalInfoViewModel.getSupportedSbasCfs())
if (!sbasCfs.isEmpty()) {
Collections.sort(sbasCfs)
body.append(
context.getString(
R.string.capability_title_sbas_cf,
IOUtils.trimEnds(sbasCfs.toString())
)
)
}
// Accumulated delta range
body.append(
context.getString(
R.string.capability_title_accumulated_delta_range,
PreferenceUtils.getCapabilityDescription(
context,
prefs.getInt(
context.getString(R.string.capability_key_measurement_delta_range),
PreferenceUtils.CAPABILITY_UNKNOWN
)
)
)
)
// Automatic gain control
body.append(
context.getString(
R.string.capability_title_automatic_gain_control,
PreferenceUtils.getCapabilityDescription(
context,
prefs.getInt(
context.getString(R.string.capability_key_measurement_automatic_gain_control),
PreferenceUtils.CAPABILITY_UNKNOWN
)
)
)
)
}
// GNSS Antenna Info
val gnssAntennaInfo = context.getString(
R.string.capability_title_gnss_antenna_info,
PreferenceUtils.getCapabilityDescription(
context,
SatelliteUtils.isGnssAntennaInfoSupported(
locationManager
)
)
)
body.append(gnssAntennaInfo)
if (gnssAntennaInfo == context.getString(R.string.capability_value_supported)) {
body.append(
context.getString(
R.string.capability_title_num_antennas, PreferenceUtils.getInt(
context.getString(R.string.capability_key_num_antenna), -1, prefs
)
)
)
body.append(
context.getString(
R.string.capability_title_antenna_cfs, PreferenceUtils.getString(
context.getString(R.string.capability_key_antenna_cf), prefs
)
)
)
}
if (!TextUtils.isEmpty(playServicesVersion)) {
body.append(
"""
$playServicesVersion
""".trimIndent()
)
}
body.append("\n\n\n")
val send = Intent(Intent.ACTION_SENDTO)
send.data = Uri.parse("mailto:")
send.putExtra(Intent.EXTRA_EMAIL, arrayOf(email))
val subject = context.getString(R.string.feedback_subject)
send.putExtra(Intent.EXTRA_SUBJECT, subject)
send.putExtra(Intent.EXTRA_TEXT, body.toString())
try {
context.startActivity(Intent.createChooser(send, subject))
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.feedback_error, Toast.LENGTH_LONG)
.show()
}
}
/**
* Returns the provided latitude or longitude value in Degrees Minutes Seconds (DMS) format
* @param coordinate latitude or longitude to convert to DMS format
* @param coordinateType whether the coordinate is latitude or longitude
* @return the provided latitude or longitude value in Degrees Minutes Seconds (DMS) format
*/
@JvmStatic
fun getDMSFromLocation(
context: Context,
coordinate: Double,
coordinateType: CoordinateType
): String {
val loc = BigDecimal(coordinate)
val degrees = loc.setScale(0, RoundingMode.DOWN)
val minTemp = loc.subtract(degrees).multiply(BigDecimal(60)).abs()
val minutes = minTemp.setScale(0, RoundingMode.DOWN)
val seconds =
minTemp.subtract(minutes).multiply(BigDecimal(60)).setScale(2, RoundingMode.HALF_UP)
val hemisphere: String
val output_string: Int
if (coordinateType == CoordinateType.LATITUDE) {
hemisphere = if (coordinate < 0) "S" else "N"
output_string = R.string.gps_lat_dms_value
} else {
hemisphere = if (coordinate < 0) "W" else "E"
output_string = R.string.gps_lon_dms_value
}
return context.getString(
output_string,
hemisphere,
degrees.abs().toInt(),
minutes.toInt(),
seconds.toFloat()
)
}
/**
* Returns the provided latitude or longitude value in Decimal Degree Minutes (DDM) format
*
* @param coordinate latitude or longitude to convert to DDM format
* @param coordinateType lat or lon to format hemisphere
* @return the provided latitude or longitude value in Decimal Degree Minutes (DDM) format
*/
@JvmStatic
fun getDDMFromLocation(
context: Context,
coordinate: Double,
coordinateType: CoordinateType
): String {
val loc = BigDecimal(coordinate)
val degrees = loc.setScale(0, RoundingMode.DOWN)
val minutes =
loc.subtract(degrees).multiply(BigDecimal(60)).abs().setScale(3, RoundingMode.HALF_UP)
val hemisphere: String
val output_string: Int
if (coordinateType == CoordinateType.LATITUDE) {
hemisphere = if (coordinate < 0) "S" else "N"
output_string = R.string.gps_lat_ddm_value
} else {
hemisphere = if (coordinate < 0) "W" else "E"
output_string = R.string.gps_lon_ddm_value
}
return context.getString(
output_string,
hemisphere,
degrees.abs().toInt(),
minutes.toFloat()
)
}
/**
* Converts the provide value in meters to the corresponding value in feet
* @param meters value in meters to convert to feet
* @return the provided meters value converted to feet
*/
@JvmStatic
fun toFeet(meters: Double): Double {
return meters * 1000.0 / 25.4 / 12.0
}
/**
* Converts the provide value in meters per second to the corresponding value in kilometers per hour
* @param metersPerSecond value in meters per second to convert to kilometers per hour
* @return the provided meters per second value converted to kilometers per hour
*/
@JvmStatic
fun toKilometersPerHour(metersPerSecond: Float): Float {
return metersPerSecond * 3600f / 1000f
}
/**
* Converts the provide value in meters per second to the corresponding value in miles per hour
* @param metersPerSecond value in meters per second to convert to miles per hour
* @return the provided meters per second value converted to miles per hour
*/
@JvmStatic
fun toMilesPerHour(metersPerSecond: Float): Float {
return toKilometersPerHour(metersPerSecond) / 1.6093440f
}
/**
* Sets the vertical bias for a provided view that is within a ConstraintLayout
* @param view view within a ConstraintLayout
* @param bias vertical bias to be used
*/
@JvmStatic
fun setVerticalBias(view: View, bias: Float) {
val params = view.layoutParams as ConstraintLayout.LayoutParams
params.verticalBias = bias
view.layoutParams = params
}
/**
* Returns the provided location based on the provided coordinate format, and sets the provided
* Views (textView, chips) accordingly if views are provided, and returns the string value.
*
* @param location location to be formatted
* @param textView View to be set with the selected coordinateFormat
* @param includeAltitude true if altitude should be included, false if it should not
* @param chipDecimalDegrees View to be set as checked if "dd" is the coordinateFormat
* @param chipDMS View to be set as checked if "dms" is the coordinateFormat
* @param chipDegreesDecimalMin View to be set as checked if "ddm" is the coordinateFormat
* @param coordinateFormat dd, dms, or ddm
* @return the provided location based on the provided coordinate format
*/
fun formatLocationForDisplay(
context: Context,
location: Location?,
textView: TextView?,
includeAltitude: Boolean,
chipDecimalDegrees: Chip?,
chipDMS: Chip?,
chipDegreesDecimalMin: Chip?,
coordinateFormat: String?
): String {
var formattedLocation = ""
when (coordinateFormat) {
"dd" -> {
// Decimal degrees
if (location != null) {
formattedLocation = IOUtils.createLocationShare(location, includeAltitude)
}
if (chipDecimalDegrees != null) {
chipDecimalDegrees.isChecked = true
}
}
"dms" -> {
// Degrees minutes seconds
if (location != null) {
formattedLocation = IOUtils.createLocationShare(
getDMSFromLocation(context, location.latitude, CoordinateType.LATITUDE),
getDMSFromLocation(context, location.longitude, CoordinateType.LONGITUDE),
if (location.hasAltitude() && includeAltitude) location.altitude.toString() else null
)
}
if (chipDMS != null) {
chipDMS.isChecked = true
}
}
"ddm" -> {
// Degrees decimal minutes
if (location != null) {
formattedLocation = IOUtils.createLocationShare(
getDDMFromLocation(context, location.latitude, CoordinateType.LATITUDE),
getDDMFromLocation(context, location.longitude, CoordinateType.LONGITUDE),
if (location.hasAltitude() && includeAltitude) location.altitude.toString() else null
)
}
if (chipDegreesDecimalMin != null) {
chipDegreesDecimalMin.isChecked = true
}
}
else -> {
// Decimal degrees
formattedLocation = IOUtils.createLocationShare(location, includeAltitude)
if (chipDecimalDegrees != null) {
chipDecimalDegrees.isChecked = true
}
}
}
if (textView != null) {
textView.text = formattedLocation
}
return formattedLocation
}
/**
* Resets the activity title so the locale is updated
*
* @param a the activity to reset the title for
*/
@JvmStatic
fun resetActivityTitle(a: Activity) {
try {
val info =
a.packageManager.getActivityInfo(a.componentName, PackageManager.GET_META_DATA)
if (info.labelRes != 0) {
a.setTitle(info.labelRes)
}
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
}
/**
* Returns true if the app is running on a large screen device, false if it is not
*
* @return true if the app is running on a large screen device, false if it is not
*/
fun isLargeScreen(context: Context): Boolean {
return (context.resources.configuration.screenLayout
and Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE
}
/**
* Returns the display name for the given GnssType
* @param context
* @param gnssType
* @return the display name for the given GnssType
*/
fun getGnssDisplayName(context: Context, gnssType: GnssType?): String {
return when (gnssType) {
GnssType.NAVSTAR -> context.resources.getString(R.string.sky_legend_shape_navstar)
GnssType.GALILEO -> context.resources.getString(R.string.sky_legend_shape_galileo)
GnssType.GLONASS -> context.resources.getString(R.string.sky_legend_shape_glonass)
GnssType.BEIDOU -> context.resources.getString(R.string.sky_legend_shape_beidou)
GnssType.QZSS -> context.resources.getString(R.string.sky_legend_shape_qzss)
GnssType.IRNSS -> context.resources.getString(R.string.sky_legend_shape_irnss)
GnssType.SBAS -> context.resources.getString(R.string.sbas)
GnssType.UNKNOWN -> context.resources.getString(R.string.unknown)
else -> context.resources.getString(R.string.unknown)
}
}
fun setClickableSpan(v: TextView, span: ClickableSpan?) {
val text = v.text as Spannable
text.setSpan(span, 0, text.length, 0)
v.movementMethod = LinkMovementMethod.getInstance()
}
fun removeAllClickableSpans(v: TextView) {
val text = v.text as Spannable
val spans = text.getSpans(0, text.length, ClickableSpan::class.java)
for (cs in spans) {
text.removeSpan(cs)
}
}
/**
* Shows a view using animation
*
* @param v View to show
* @param animationDuration duration of animation
*/
fun showViewWithAnimation(v: View, animationDuration: Int) {
if (v.visibility == View.VISIBLE && v.alpha == 1f) {
// View is already visible and not transparent, return without doing anything
return
}
v.clearAnimation()
v.animate().cancel()
if (v.visibility != View.VISIBLE) {
// Set the content view to 0% opacity but visible, so that it is visible
// (but fully transparent) during the animation.
v.alpha = 0f
v.visibility = View.VISIBLE
}
// Animate the content view to 100% opacity, and clear any animation listener set on the view.
v.animate()
.alpha(1f)
.setDuration(animationDuration.toLong())
.setListener(null)
}
/**
* Hides a view using animation
*
* @param v View to hide
* @param animationDuration duration of animation
*/
fun hideViewWithAnimation(v: View, animationDuration: Int) {
if (v.visibility == View.GONE) {
// View is already gone, return without doing anything
return
}
v.clearAnimation()
v.animate().cancel()
// Animate the view to 0% opacity. After the animation ends, set its visibility to GONE as
// an optimization step (it won't participate in layout passes, etc.)
v.animate()
.alpha(0f)
.setDuration(animationDuration.toLong())
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
v.visibility = View.GONE
}
})
}
/**
* Shows the dialog to explain why location permissions are needed
*
* NOTE - this dialog can't be managed under the old dialog framework as the method
* ActivityCompat.shouldShowRequestPermissionRationale() always returns false.
*/
fun showLocationPermissionDialog(activity: Activity) {
val builder = AlertDialog.Builder(activity)
.setTitle(R.string.title_location_permission)
.setMessage(R.string.text_location_permission)
.setCancelable(false)
.setPositiveButton(
R.string.ok
) { dialog: DialogInterface?, which: Int ->
// Request permissions from the user
ActivityCompat.requestPermissions(
activity,
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
),
PermissionUtils.LOCATION_PERMISSION_REQUEST
)
}
.setNegativeButton(
R.string.exit
) { dialog: DialogInterface?, which: Int ->
// Exit app
activity.finish()
}
builder.create().show()
}
/**
* Ask the user if they want to enable GPS, and if so, show them system settings
*/
fun promptEnableGps(context: Context,activity: Activity) {
AlertDialog.Builder(activity)
.setMessage(context.getString(R.string.enable_gps_message))
.setPositiveButton(
context.getString(R.string.enable_gps_positive_button)
) { dialog: DialogInterface?, which: Int ->
val intent = Intent(
Settings.ACTION_LOCATION_SOURCE_SETTINGS
)
activity.startActivity(intent)
}
.setNegativeButton(
context.getString(R.string.enable_gps_negative_button)
) { dialog: DialogInterface?, which: Int -> }
.show()
}
/**
* Saves the "sort by" order to preferences
*
* @param index the index of R.array.sort_sats that should be set
*/
public fun setSortByClause(context: Context, index: Int, prefs: SharedPreferences) {
val sortOptions = context.resources.getStringArray(R.array.sort_sats)
PreferenceUtils.saveString(
context.resources.getString(R.string.pref_key_default_sat_sort),
sortOptions[index],
prefs
)
}
/**
* Show the "What's New" message if a new version was just installed
*/
fun autoShowWhatsNew(prefs: SharedPreferences, context: Context, activity: Activity) {
// Get the current app version.
val appInfo: PackageInfo = try {
context.packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_META_DATA
)
} catch (e: PackageManager.NameNotFoundException) {
// Do nothing
return
}
val oldVer = prefs.getInt(WHATS_NEW_VER, 0)
val newVer = appInfo.versionCode
if (oldVer < newVer) {
activity.showDialog(WHATSNEW_DIALOG)
PreferenceUtils.saveInt(WHATS_NEW_VER, appInfo.versionCode, prefs)
}
}
/**
* Returns the `location` object as a human readable string for use in a notification summary
*/
fun Location?.toNotificationSummary(context: Context, prefs: SharedPreferences): String {
return if (this != null) {
val lat = FormatUtils.formatLatOrLon(context, latitude, CoordinateType.LATITUDE, prefs)
val lon = FormatUtils.formatLatOrLon(context, longitude, CoordinateType.LONGITUDE, prefs)
val alt = FormatUtils.formatAltitude(context, this, prefs)
val speed = FormatUtils.formatSpeed(context,this, prefs)
val bearing = FormatUtils.formatBearing(context,this)
"$lat $lon $alt | $speed | $bearing"
} else {
"Unknown location"
}
}
}

View File

@@ -2,7 +2,7 @@
* Based on https://github.com/YarikSOffice/LanguageTest/blob/master/app/src/main/java/com/yariksoffice/languagetest/Utility.java
* Licensed under MIT - https://github.com/YarikSOffice/LanguageTest/blob/master/LICENSE
*/
package com.android.gpstest.util;
package com.android.gpstest.library.util;
import android.os.Build;

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.util;
package com.android.gpstest.library.util;
import android.location.Location;
import android.os.SystemClock;

View File

@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.util;
package com.android.gpstest.library.util;
import android.util.Base64;

View File

@@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.util;
package com.android.gpstest.library.util;
import android.text.TextUtils;
import android.util.Log;
import com.android.gpstest.model.DilutionOfPrecision;
import com.android.gpstest.library.model.DilutionOfPrecision;
public class NmeaUtils {

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.util
package com.android.gpstest.library.util
import android.Manifest
import android.content.Context

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.util;
package com.android.gpstest.library.util;
import android.Manifest;
import android.app.Activity;

View File

@@ -0,0 +1,283 @@
package com.android.gpstest.library.util
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.location.GnssMeasurementsEvent
import android.location.LocationManager
import android.os.Build
import com.android.gpstest.library.R
/**
* Provides access to SharedPreferences to Activities and Services.
*/
object PreferenceUtil {
const val SECONDS_TO_MILLISECONDS = 1000
val METERS = "1"
val METERS_PER_SECOND = "1"
val KILOMETERS_PER_HOUR = "2"
/**
* Returns the minTime between location updates used for the LocationListener in milliseconds
*/
fun minTimeMillis(context: Context, prefs: SharedPreferences): Long {
val minTimeDouble: Double =
prefs.getString(context.getString(R.string.pref_key_gps_min_time), "1")
?.toDouble() ?: 1.0
return (minTimeDouble * SECONDS_TO_MILLISECONDS).toLong()
}
/**
* Returns the minDistance between location updates used for the LocationLitsener in meters
*/
fun minDistance(context: Context, prefs: SharedPreferences): Float {
return prefs.getString(context.getString(R.string.pref_key_gps_min_distance), "0") ?.toFloat() ?: 0.0f
}
/**
* Returns true if the user has selected to write locations to file output, false if they have not
*/
fun writeLocationToFile(context: Context, prefs: SharedPreferences): Boolean {
return prefs.getBoolean(context.getString(R.string.pref_key_file_location_output), false)
}
fun writeMeasurementToLogcat(context: Context, prefs: SharedPreferences): Boolean {
return prefs.getBoolean(context.getString(R.string.pref_key_as_measurement_output), false)
}
fun writeMeasurementsToFile(context: Context, prefs: SharedPreferences): Boolean {
return prefs.getBoolean(context.getString(R.string.pref_key_file_measurement_output), false)
}
fun writeNmeaToAndroidMonitor(context: Context, prefs: SharedPreferences): Boolean {
return prefs.getBoolean(context.getString(R.string.pref_key_as_nmea_output), true)
}
fun writeNmeaTimestampToLogcat(context: Context, prefs: SharedPreferences): Boolean {
return prefs.getBoolean(context.getString(R.string.pref_key_as_nmea_timestamp_output), true)
}
fun writeNmeaToFile(context: Context, prefs: SharedPreferences): Boolean {
return prefs.getBoolean(context.getString(R.string.pref_key_file_nmea_output), false)
}
fun writeAntennaInfoToFileJson(context: Context, prefs: SharedPreferences): Boolean {
return prefs.getBoolean(context.getString(R.string.pref_key_file_antenna_output_json), false);
}
fun writeAntennaInfoToFileCsv(context: Context, prefs: SharedPreferences): Boolean {
return prefs.getBoolean(context.getString(R.string.pref_key_file_antenna_output_csv), false)
}
fun writeNavMessageToLogcat(context: Context, prefs: SharedPreferences): Boolean {
return prefs.getBoolean(context.getString(R.string.pref_key_as_navigation_message_output), false);
}
fun writeNavMessageToFile(context: Context, prefs: SharedPreferences): Boolean {
return prefs.getBoolean(context.getString(R.string.pref_key_file_navigation_message_output), false);
}
fun writeStatusToFile(context: Context, prefs: SharedPreferences): Boolean {
return prefs.getBoolean(context.getString(R.string.pref_key_file_gnss_status_output), false);
}
fun writeOrientationToFile(context: Context, prefs: SharedPreferences): Boolean {
return prefs.getBoolean(context.getString(R.string.pref_key_file_orientation_output), false);
}
fun injectTimeWhenLogging(context: Context, prefs: SharedPreferences): Boolean {
return prefs.getBoolean(context.getString(R.string.pref_key_inject_time_when_logging), true);
}
fun injectPsdsWhenLogging(context: Context, prefs: SharedPreferences): Boolean {
return prefs.getBoolean(context.getString(R.string.pref_key_inject_psds_when_logging), true);
}
fun isFileLoggingEnabled(context: Context, prefs: SharedPreferences): Boolean {
return isCsvLoggingEnabled(context, prefs) || isJsonLoggingEnabled(context, prefs)
}
fun isCsvLoggingEnabled(context: Context, prefs: SharedPreferences): Boolean {
return writeNmeaToFile(context, prefs) || writeMeasurementsToFile(context, prefs) || writeNavMessageToFile(context, prefs) || writeLocationToFile(context, prefs) || writeAntennaInfoToFileCsv(context, prefs) || writeStatusToFile(context, prefs) || writeOrientationToFile(context, prefs)
}
fun isJsonLoggingEnabled(context: Context, prefs: SharedPreferences): Boolean {
return writeAntennaInfoToFileJson(context, prefs)
}
fun distanceUnits(context: Context, prefs: SharedPreferences): String {
return prefs.getString(context.getString(R.string.pref_key_preferred_distance_units_v2), METERS) ?: METERS
}
fun speedUnits(context: Context, prefs: SharedPreferences): String {
return prefs.getString(context.getString(R.string.pref_key_preferred_speed_units_v2), METERS_PER_SECOND) ?: METERS_PER_SECOND
}
fun coordinateFormat(context: Context, prefs: SharedPreferences): String {
return prefs.getString(
context.getString(R.string.pref_key_coordinate_format),
context.getString(R.string.preferences_coordinate_format_dd_key)
) ?: context.getString(R.string.preferences_coordinate_format_dd_key)
}
fun runInBackground(context: Context, prefs: SharedPreferences): Boolean {
return prefs.getBoolean(context.getString(R.string.pref_key_gnss_background), false)
}
fun darkTheme(context: Context, prefs: SharedPreferences): Boolean {
return prefs.getBoolean(context.getString(R.string.pref_key_dark_theme), false)
}
fun shareIncludeAltitude(context: Context, prefs: SharedPreferences): Boolean {
return prefs.getBoolean(context.getString(R.string.pref_key_share_include_altitude), false
)
}
/**
* Returns true if preferences related to raw measurements should be enabled,
* false if they should be disabled
*/
fun enableMeasurementsPref(context: Context, prefs: SharedPreferences): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
return SatelliteUtils.isMeasurementsSupported(manager)
}
// Legacy versions before Android S
val capabilityMeasurementsInt = prefs.getInt(
context.getString(R.string.capability_key_raw_measurements),
PreferenceUtils.CAPABILITY_UNKNOWN
)
return capabilityMeasurementsInt != PreferenceUtils.CAPABILITY_NOT_SUPPORTED
}
/**
* Returns true if preferences related to navigation messages should be enabled,
* false if they should be disabled
*/
fun enableNavMessagesPref(context: Context, prefs: SharedPreferences): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
return SatelliteUtils.isNavMessagesSupported(manager)
}
// Legacy versions before Android S
val capabilityNavMessagesInt = prefs.getInt(
context.getString(R.string.capability_key_nav_messages),
PreferenceUtils.CAPABILITY_UNKNOWN
)
return capabilityNavMessagesInt != PreferenceUtils.CAPABILITY_NOT_SUPPORTED
}
/**
* Saves device capabilities for GNSS measurements and related information from the given [event]
*/
fun saveMeasurementCapabilities(context: Context, event: GnssMeasurementsEvent, prefs: SharedPreferences) {
var agcSupport = PreferenceUtils.CAPABILITY_UNKNOWN
var carrierPhaseSupport = PreferenceUtils.CAPABILITY_UNKNOWN
// Loop through all measurements - if at least one supports, then mark as supported
for (measurement in event.measurements) {
if (SatelliteUtils.isAutomaticGainControlSupported(measurement)) {
agcSupport = PreferenceUtils.CAPABILITY_SUPPORTED
} else if (agcSupport == PreferenceUtils.CAPABILITY_UNKNOWN) {
agcSupport = PreferenceUtils.CAPABILITY_NOT_SUPPORTED
}
if (SatelliteUtils.isCarrierPhaseSupported(measurement)) {
carrierPhaseSupport = PreferenceUtils.CAPABILITY_SUPPORTED
} else if (carrierPhaseSupport == PreferenceUtils.CAPABILITY_UNKNOWN) {
carrierPhaseSupport = PreferenceUtils.CAPABILITY_NOT_SUPPORTED
}
}
PreferenceUtils.saveInt(
context.getString(R.string.capability_key_measurement_automatic_gain_control),
agcSupport,
prefs
)
PreferenceUtils.saveInt(
context.getString(R.string.capability_key_measurement_delta_range),
carrierPhaseSupport,
prefs
)
}
/**
* Creates a new preference listener that will invoke the provide [cancelFlows] function (e.g., to cancel jobs)
* when the user turns off tracking via the UI.
*
* Returns a reference to the OnSharedPreferenceChangeListener so it can be held by the calling class, as
* anonymous preference listeners tend to get GC'd by Android.
*/
fun newStopTrackingListener(cancelFlows: () -> Unit, prefs: SharedPreferences): SharedPreferences.OnSharedPreferenceChangeListener {
return SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (key == PreferenceUtils.KEY_SERVICE_TRACKING_ENABLED) {
if (!PreferenceUtils.isTrackingStarted(prefs)) {
cancelFlows()
}
}
}
}
/**
* Creates a new preference listener that will invoke the provide [initLogging] function
* when the user turns on file logging via any of the Settings options and all of the other file
* logging settings were previously off
* @param context
* @param initLogging
* @param prefs
* Returns a reference to the OnSharedPreferenceChangeListener so it can be held by the calling class, as
* anonymous preference listeners tend to get GC'd by Android.
*/
fun newFileLoggingListener(context: Context, initLogging: () -> Unit, prefs: SharedPreferences): SharedPreferences.OnSharedPreferenceChangeListener {
return SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (key == context.getString(R.string.pref_key_file_location_output) ||
key == context.getString(R.string.pref_key_file_measurement_output) ||
key == context.getString(R.string.pref_key_file_nmea_output) ||
key == context.getString(R.string.pref_key_file_navigation_message_output) ||
key == context.getString(R.string.pref_key_file_antenna_output_csv) ||
key == context.getString(R.string.pref_key_file_antenna_output_json)) {
// Count number of file logging preferences that are enabled
val loggingEnabled = arrayOf(writeLocationToFile(context, prefs), writeMeasurementsToFile(context, prefs), writeNmeaToFile(context, prefs), writeNavMessageToFile(context, prefs), writeAntennaInfoToFileCsv(context, prefs), writeAntennaInfoToFileJson(context, prefs))
val enabledCount = loggingEnabled.count { it }
if (enabledCount == 1) {
if (key == context.getString(R.string.pref_key_file_location_output) &&
writeLocationToFile(context, prefs)
) {
// Location file logging was just enabled
initLogging()
}
if (key == context.getString(R.string.pref_key_file_measurement_output) &&
writeMeasurementsToFile(context, prefs)
) {
// Measurement file logging was just enabled
initLogging()
}
if (key == context.getString(R.string.pref_key_file_nmea_output) &&
writeNmeaToFile(context, prefs)
) {
// NMEA file logging was just enabled
initLogging()
}
if (key == context.getString(R.string.pref_key_file_navigation_message_output) &&
writeNavMessageToFile(context, prefs)
) {
// Nav message file logging was just enabled
initLogging()
}
if (key == context.getString(R.string.pref_key_file_antenna_output_csv) &&
writeAntennaInfoToFileCsv(context, prefs)
) {
// Antenna CSV file logging was just enabled
initLogging()
}
if (key == context.getString(R.string.pref_key_file_antenna_output_json) &&
writeAntennaInfoToFileJson(context,prefs)
) {
// Antenna JSON file logging was just enabled
initLogging()
}
}
}
}
}
}

View File

@@ -13,17 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.util;
package com.android.gpstest.library.util;
import static java.util.Collections.emptySet;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import com.android.gpstest.Application;
import com.android.gpstest.R;
import com.android.gpstest.model.GnssType;
import com.android.gpstest.library.R;
import com.android.gpstest.library.model.GnssType;
import java.util.LinkedHashSet;
import java.util.Set;
@@ -45,18 +44,18 @@ public class PreferenceUtils {
* @param capability CAPABILITY_* constant defined in this class
* @return a string description of the CAPABILITY_* constant
*/
public static String getCapabilityDescription(int capability) {
public static String getCapabilityDescription(Context context, int capability) {
switch (capability) {
case CAPABILITY_UNKNOWN:
return Application.Companion.getApp().getString(R.string.capability_value_unknown);
return context.getString(R.string.capability_value_unknown);
case CAPABILITY_NOT_SUPPORTED:
return Application.Companion.getApp().getString(R.string.capability_value_not_supported);
return context.getString(R.string.capability_value_not_supported);
case CAPABILITY_SUPPORTED:
return Application.Companion.getApp().getString(R.string.capability_value_supported);
return context.getString(R.string.capability_value_supported);
case CAPABILITY_LOCATION_DISABLED:
return Application.Companion.getApp().getString(R.string.capability_value_location_disabled);
return context.getString(R.string.capability_value_location_disabled);
default:
return Application.Companion.getApp().getString(R.string.capability_value_unknown);
return context.getString(R.string.capability_value_unknown);
}
}
@@ -65,85 +64,60 @@ public class PreferenceUtils {
* @param supported
* @return a simple SUPPORTED or UNSUPPORTED value for the given boolean value
*/
public static String getCapabilityDescription(boolean supported) {
public static String getCapabilityDescription(Context context, boolean supported) {
if (supported) {
return Application.Companion.getApp().getString(R.string.capability_value_supported);
return context.getString(R.string.capability_value_supported);
} else {
return Application.Companion.getApp().getString(R.string.capability_value_not_supported);
return context.getString(R.string.capability_value_not_supported);
}
}
@TargetApi(9)
public static void saveString(SharedPreferences prefs, String key, String value) {
public static void saveString(String key, String value, SharedPreferences prefs) {
SharedPreferences.Editor edit = prefs.edit();
edit.putString(key, value);
edit.apply();
}
public static void saveString(String key, String value) {
saveString(Application.Companion.getPrefs(), key, value);
}
@TargetApi(9)
public static void saveInt(SharedPreferences prefs, String key, int value) {
public static void saveInt(String key, int value, SharedPreferences prefs) {
SharedPreferences.Editor edit = prefs.edit();
edit.putInt(key, value);
edit.apply();
}
public static void saveInt(String key, int value) {
saveInt(Application.Companion.getPrefs(), key, value);
}
public static int getInt(String key, int defaultValue) {
return Application.Companion.getPrefs().getInt(key, defaultValue);
public static int getInt(String key, int defaultValue, SharedPreferences prefs) {
return prefs.getInt(key, defaultValue);
}
@TargetApi(9)
public static void saveLong(SharedPreferences prefs, String key, long value) {
public static void saveLong(String key, long value, SharedPreferences prefs) {
SharedPreferences.Editor edit = prefs.edit();
edit.putLong(key, value);
edit.apply();
}
public static void saveLong(String key, long value) {
saveLong(Application.Companion.getPrefs(), key, value);
}
@TargetApi(9)
public static void saveBoolean(SharedPreferences prefs, String key, boolean value) {
public static void saveBoolean(String key, boolean value, SharedPreferences prefs) {
SharedPreferences.Editor edit = prefs.edit();
edit.putBoolean(key, value);
edit.apply();
}
public static void saveBoolean(String key, boolean value) {
saveBoolean(Application.Companion.getPrefs(), key, value);
}
@TargetApi(9)
public static void saveFloat(SharedPreferences prefs, String key, float value) {
public static void saveFloat(String key, float value, SharedPreferences prefs) {
SharedPreferences.Editor edit = prefs.edit();
edit.putFloat(key, value);
edit.apply();
}
public static void saveFloat(String key, float value) {
saveFloat(Application.Companion.getPrefs(), key, value);
}
@TargetApi(9)
public static void saveDouble(SharedPreferences prefs, String key, double value) {
public static void saveDouble(String key, double value, SharedPreferences prefs) {
SharedPreferences.Editor edit = prefs.edit();
edit.putLong(key, Double.doubleToRawLongBits(value));
edit.apply();
}
@TargetApi(9)
public static void saveDouble(String key, double value) {
saveDouble(Application.Companion.getPrefs(), key, value);
}
/**
* Gets a double for the provided key from preferences, or the default value if the preference
* doesn't currently have a value
@@ -152,23 +126,23 @@ public class PreferenceUtils {
* @param defaultValue the default value to return if the key doesn't have a value
* @return a double from preferences, or the default value if it doesn't exist
*/
public static Double getDouble(String key, double defaultValue) {
if (!Application.Companion.getPrefs().contains(key)) {
public static Double getDouble(String key, double defaultValue, SharedPreferences prefs) {
if (!prefs.contains(key)) {
return defaultValue;
}
return Double.longBitsToDouble(Application.Companion.getPrefs().getLong(key, 0));
return Double.longBitsToDouble(prefs.getLong(key, 0));
}
public static String getString(String key) {
return Application.Companion.getPrefs().getString(key, null);
public static String getString(String key, SharedPreferences prefs) {
return prefs.getString(key, null);
}
public static long getLong(String key, long defaultValue) {
return Application.Companion.getPrefs().getLong(key, defaultValue);
public static long getLong(SharedPreferences prefs, String key, long defaultValue) {
return prefs.getLong(key, defaultValue);
}
public static float getFloat(String key, float defaultValue) {
return Application.Companion.getPrefs().getFloat(key, defaultValue);
public static float getFloat(SharedPreferences prefs, String key, float defaultValue) {
return prefs.getFloat(key, defaultValue);
}
/**
@@ -176,9 +150,9 @@ public class PreferenceUtils {
*
* @return the currently selected satellite sort order as the index in R.array.sort_sats
*/
public static int getSatSortOrderFromPreferences() {
Resources r = Application.Companion.getApp().getResources();
SharedPreferences settings = Application.Companion.getPrefs();
public static int getSatSortOrderFromPreferences(Context context, SharedPreferences prefs) {
Resources r = context.getResources();
SharedPreferences settings = prefs;
String[] sortOptions = r.getStringArray(R.array.sort_sats);
String sortPref = settings.getString(r.getString(
R.string.pref_key_default_sat_sort), sortOptions[0]);
@@ -194,10 +168,10 @@ public class PreferenceUtils {
* Gets a set of GnssTypes that should have their satellites displayed that has been saved to preferences. (All are shown if empty or null)
* @return a set of GnssTypes that should have their satellites displayed that has been saved to preferences. (All are shown if empty or null)
*/
public static Set<GnssType> gnssFilter() {
public static Set<GnssType> gnssFilter(Context context, SharedPreferences prefs) {
Set<GnssType> filter = new LinkedHashSet<>();
Resources r = Application.Companion.getApp().getResources();
String filterString = getString(r.getString(R.string.pref_key_default_sat_filter));
Resources r = context.getResources();
String filterString = getString(r.getString(R.string.pref_key_default_sat_filter), prefs);
if (filterString == null) {
return filter;
}
@@ -216,8 +190,8 @@ public class PreferenceUtils {
* Values are persisted as string of comma-separated values, with each of the enum values .toString() called
* @param filter a set of GnssTypes that should have their satellites displayed. (All are shown if empty or null)
*/
public static void saveGnssFilter(Set<GnssType> filter) {
Resources r = Application.Companion.getApp().getResources();
public static void saveGnssFilter(Context context, Set<GnssType> filter, SharedPreferences prefs) {
Resources r = context.getResources();
StringBuilder filterString = new StringBuilder();
for (GnssType gnssType : filter) {
filterString.append(gnssType.toString() + ",");
@@ -226,22 +200,22 @@ public class PreferenceUtils {
if (filter.size() >= 1) {
filterString.deleteCharAt(filterString.length() - 1);
}
saveString(r.getString(R.string.pref_key_default_sat_filter), filterString.toString());
saveString(r.getString(R.string.pref_key_default_sat_filter), filterString.toString(), prefs);
}
/**
* Clears any active GNSS filter so all satellites are displayed
*/
public static void clearGnssFilter() {
saveGnssFilter(emptySet());
public static void clearGnssFilter(Context context, SharedPreferences prefs) {
saveGnssFilter(context, emptySet(), prefs);
}
/**
* Removes the specified preference by deleting it
* @param key
*/
public static void remove(String key) {
SharedPreferences.Editor edit = Application.Companion.getPrefs().edit();
public static void remove(String key, SharedPreferences prefs) {
SharedPreferences.Editor edit = prefs.edit();
edit.remove(key).apply();
}
@@ -249,15 +223,15 @@ public class PreferenceUtils {
* Returns true if service location tracking is active, and false if it is not
* @return true if service location tracking is active, and false if it is not
*/
public static boolean isTrackingStarted() {
return Application.Companion.getPrefs().getBoolean(KEY_SERVICE_TRACKING_ENABLED, false);
public static boolean isTrackingStarted(SharedPreferences prefs) {
return prefs.getBoolean(KEY_SERVICE_TRACKING_ENABLED, false);
}
/**
* Saves the provided value as the current service location tracking state
* @param value true if service location tracking is active, and false if it is not
*/
public static void saveTrackingStarted(boolean value) {
saveBoolean(KEY_SERVICE_TRACKING_ENABLED, value);
public static void saveTrackingStarted(boolean value, SharedPreferences prefs) {
saveBoolean(KEY_SERVICE_TRACKING_ENABLED, value, prefs);
}
}

View File

@@ -13,17 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.util
package com.android.gpstest.library.util
import android.annotation.SuppressLint
import android.location.GnssStatus
import android.location.Location
import android.os.Build
import com.android.gpstest.model.*
import com.android.gpstest.util.CarrierFreqUtils.*
import com.android.gpstest.util.SatelliteUtils.createGnssSatelliteKey
import com.android.gpstest.library.model.*
import com.android.gpstest.library.util.CarrierFreqUtils.*
import com.android.gpstest.library.util.SatelliteUtils.createGnssSatelliteKey
internal object SatelliteUtil {
object SatelliteUtil {
/**
* Tranforms the Android [GnssStatus] object to a list of our [SatelliteStatus] model objects

View File

@@ -14,9 +14,9 @@
* limitations under the License.
*/
package com.android.gpstest.util;
package com.android.gpstest.library.util;
import static com.android.gpstest.model.GnssType.SBAS;
import static com.android.gpstest.library.model.GnssType.SBAS;
import android.content.Context;
import android.hardware.Sensor;
@@ -27,9 +27,9 @@ import android.os.Build;
import androidx.annotation.RequiresApi;
import com.android.gpstest.model.GnssType;
import com.android.gpstest.model.SatelliteName;
import com.android.gpstest.model.SatelliteStatus;
import com.android.gpstest.library.model.GnssType;
import com.android.gpstest.library.model.SatelliteName;
import com.android.gpstest.library.model.SatelliteStatus;
/**
* Utilities to manage GNSS signal and satellite information

View File

@@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gpstest.util
package com.android.gpstest.library.util
import com.android.gpstest.model.SatelliteStatus
import com.android.gpstest.library.model.SatelliteStatus
/**
* Utilities for sorting lists. Java Comparator.comparing() is only available on API 24 and higher,
@@ -41,7 +41,8 @@ class SortUtil {
* Sorts the [list] by the SatelliteStatus usedInFix desc then svid asc and returns the sorted list
*/
fun sortByUsedThenId(list: List<SatelliteStatus>): MutableList<SatelliteStatus> {
return list.sortedWith(compareByDescending(SatelliteStatus::usedInFix).thenComparing(SatelliteStatus::svid)).toMutableList()
return list.sortedWith(compareByDescending(SatelliteStatus::usedInFix).thenComparing(
SatelliteStatus::svid)).toMutableList()
}
/**
@@ -62,28 +63,32 @@ class SortUtil {
* Sorts the [list] by the SatelliteStatus gnssType then usedInFix desc then svid asc and returns the sorted list
*/
fun sortByGnssThenUsedThenId(list: List<SatelliteStatus>): MutableList<SatelliteStatus> {
return list.sortedWith(compareBy(SatelliteStatus::gnssType).thenByDescending(SatelliteStatus::usedInFix).thenComparing(SatelliteStatus::svid)).toMutableList()
return list.sortedWith(compareBy(SatelliteStatus::gnssType).thenByDescending(
SatelliteStatus::usedInFix).thenComparing(SatelliteStatus::svid)).toMutableList()
}
/**
* Sorts the [list] by the SatelliteStatus sbasType then usedInFix desc then svid asc and returns the sorted list
*/
fun sortBySbasThenUsedThenId(list: List<SatelliteStatus>): MutableList<SatelliteStatus> {
return list.sortedWith(compareBy(SatelliteStatus::sbasType).thenByDescending(SatelliteStatus::usedInFix).thenComparing(SatelliteStatus::svid)).toMutableList()
return list.sortedWith(compareBy(SatelliteStatus::sbasType).thenByDescending(
SatelliteStatus::usedInFix).thenComparing(SatelliteStatus::svid)).toMutableList()
}
/**
* Sorts the [list] by the SatelliteStatus gnssType then C/N0 desc and returns the sorted list
*/
fun sortByGnssThenCn0ThenId(list: List<SatelliteStatus>): MutableList<SatelliteStatus> {
return list.sortedWith(compareBy(SatelliteStatus::gnssType).thenByDescending(SatelliteStatus::cn0DbHz)).toMutableList()
return list.sortedWith(compareBy(SatelliteStatus::gnssType).thenByDescending(
SatelliteStatus::cn0DbHz)).toMutableList()
}
/**
* Sorts the [list] by the SatelliteStatus sbasType then C/N0 desc and returns the sorted list
*/
fun sortBySbasThenCn0ThenId(list: List<SatelliteStatus>): MutableList<SatelliteStatus> {
return list.sortedWith(compareBy(SatelliteStatus::sbasType).thenByDescending(SatelliteStatus::cn0DbHz)).toMutableList()
return list.sortedWith(compareBy(SatelliteStatus::sbasType).thenByDescending(
SatelliteStatus::cn0DbHz)).toMutableList()
}
/**

View File

@@ -1,7 +1,7 @@
/**
* From https://stackoverflow.com/a/7855852/937715
*/
package com.android.gpstest.view;
package com.android.gpstest.library.view;
import android.content.Context;
import android.graphics.Canvas;

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 161 B

After

Width:  |  Height:  |  Size: 161 B

View File

Before

Width:  |  Height:  |  Size: 698 B

After

Width:  |  Height:  |  Size: 698 B

Some files were not shown because too many files have changed in this diff Show More