Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90e0d9f9be | ||
|
|
80b6848a09 | ||
|
|
c5a3d684e6 | ||
|
|
f549508ad3 | ||
|
|
a3dfa50b99 | ||
|
|
640bd4b9a7 | ||
|
|
2b1ed93629 | ||
|
|
148b609e37 | ||
|
|
acea0e0249 | ||
|
|
9fd28d5a61 | ||
|
|
4ac38c380f | ||
|
|
3b23f9514b | ||
|
|
726481d50c | ||
|
|
c304bc8fdb | ||
|
|
6eaabe2f8c | ||
|
|
6994b33a8c | ||
|
|
25902ed60f | ||
|
|
1eb6ed9477 | ||
|
|
acc19cff01 | ||
|
|
8a700e987e | ||
|
|
4e7c77fbb2 | ||
|
|
91378d5f79 | ||
|
|
5d4a0f9a60 | ||
|
|
8f07afeb83 | ||
|
|
2196e66428 | ||
|
|
8c050491a3 | ||
|
|
b54c203e1b | ||
|
|
b7b3fbc66e | ||
|
|
bf0bb3a54d | ||
|
|
4292ea40da | ||
|
|
9e8f9734be | ||
|
|
a057fe448f | ||
|
|
aa8a6ff4fe | ||
|
|
b069b82802 | ||
|
|
1dab959ba4 | ||
|
|
8c258b0cca | ||
|
|
221e46cb30 | ||
|
|
58a643e1f0 | ||
|
|
680b5fde9d | ||
|
|
fd26076608 | ||
|
|
9afca7a975 | ||
|
|
822888dcfe | ||
|
|
4c3d3865fe | ||
|
|
3cea066d9c | ||
|
|
74645d72dd | ||
|
|
f7567f1a1d | ||
|
|
3dd022876b | ||
|
|
e2825cab31 | ||
|
|
23f0de3d7b | ||
|
|
dea3e77225 | ||
|
|
3b17dff8b8 | ||
|
|
b0e718670d | ||
|
|
592c69d18d | ||
|
|
bd4fead10f | ||
|
|
63c9ffdd3a | ||
|
|
1e71bb9896 | ||
|
|
2a64a40fd9 | ||
|
|
c93d05b252 | ||
|
|
271708ab2d | ||
|
|
9ba377a60b | ||
|
|
51050de1f9 | ||
|
|
613a2648e5 | ||
|
|
95c6d99e21 | ||
|
|
d91808b081 | ||
|
|
7f6d6305eb | ||
|
|
a6b0acc432 | ||
|
|
f4ce43ebb3 | ||
|
|
779239182c | ||
|
|
08cb7b5cf8 | ||
|
|
4b8b8a9e66 | ||
|
|
9f0b77ceb4 | ||
|
|
7995c01e50 | ||
|
|
5130548ed8 | ||
|
|
14ad3558af | ||
|
|
5d3fe854b7 | ||
|
|
7f56a0e849 | ||
|
|
b4f18fc345 | ||
|
|
c240fefcc4 | ||
|
|
f672d0e4c4 | ||
|
|
cd43cd688f | ||
|
|
881dafc588 | ||
|
|
d859f10fa0 | ||
|
|
1cefdf01df | ||
|
|
6ba53f1948 | ||
|
|
2840a0f00b | ||
|
|
c1ffd27b53 | ||
|
|
ab4a4ac9ce | ||
|
|
a7ad3743a4 | ||
|
|
23b482d505 | ||
|
|
3fec6bf919 | ||
|
|
2a6b14ca8e | ||
|
|
82801cc814 | ||
|
|
69415661dd | ||
|
|
baed7e3dd7 |
14
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
commit-message:
|
||||
prefix: "actions-update"
|
||||
labels: [ 'enhancement' ]
|
||||
schedule:
|
||||
interval: "daily"
|
||||
groups:
|
||||
actions-dependencies:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "*"
|
||||
4
.github/stale.yml
vendored
@@ -1,7 +1,7 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 120
|
||||
daysUntilStale: 400
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 120
|
||||
daysUntilClose: 400
|
||||
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
|
||||
155
.github/workflows/android.yml
vendored
@@ -12,27 +12,146 @@ on:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
test:
|
||||
runs-on: macos-latest
|
||||
unit-test:
|
||||
name: Perform checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden the runner (audit mode)
|
||||
uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
|
||||
- name: Setup Gradle and cache
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: wrapper
|
||||
|
||||
- name: Run checks
|
||||
run: ./gradlew check
|
||||
|
||||
- name: Upload SARIF report
|
||||
uses: github/codeql-action/upload-sarif@fe4161a26a8629af62121b670040955b330f9af2 # v3.29.5
|
||||
if: success() || failure()
|
||||
with:
|
||||
sarif_file: GPSTest/build/reports/lint-results-googleDebug.sarif
|
||||
category: lint
|
||||
|
||||
build:
|
||||
name: Build debug APK
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden the runner (audit mode)
|
||||
uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2
|
||||
with:
|
||||
log-accepted-android-sdk-licenses: false
|
||||
|
||||
- name: Setup Gradle and cache
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: wrapper
|
||||
cache-read-only: true
|
||||
|
||||
- name: Build app
|
||||
run: ./gradlew assembleDebug --stacktrace
|
||||
|
||||
- name: Upload APKs
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: apks
|
||||
path: |
|
||||
GPSTest/build/outputs/apk/
|
||||
wear/build/outputs/apk/
|
||||
|
||||
instrumentation-test:
|
||||
name: Run instrumented tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
api-level: [21, 23, 29]
|
||||
api-level: [24, 29, 33]
|
||||
target: [default, google_apis]
|
||||
tasks: [connectedGoogleDebugAndroidTest, connectedOsmdroidDebugAndroidTest]
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Harden the runner (audit mode)
|
||||
uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 11
|
||||
distribution: 'adopt'
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: run tests
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
target: ${{ matrix.target }}
|
||||
arch: x86_64
|
||||
profile: Nexus 6
|
||||
script: ./gradlew test check connectedCheck -x lint --stacktrace
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Setup Gradle and cache
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: wrapper
|
||||
cache-read-only: true
|
||||
|
||||
- name: Configure AVD cache
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
id: avd-cache
|
||||
with:
|
||||
path: |
|
||||
~/.android/avd/*
|
||||
~/.android/adb*
|
||||
key: avd-${{ matrix.target }}-${{ matrix.api-level }}
|
||||
|
||||
- name: Create AVD and generate snapshot for caching
|
||||
if: steps.avd-cache.outputs.cache-hit != 'true'
|
||||
uses: reactivecircus/android-emulator-runner@b530d96654c385303d652368551fb075bc2f0b6b # v2.35.0
|
||||
with:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
target: ${{ matrix.target }}
|
||||
force-avd-creation: false
|
||||
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-metrics
|
||||
disable-animations: false
|
||||
script: echo "Generated AVD snapshot for caching."
|
||||
arch: x86_64
|
||||
profile: Nexus 6
|
||||
|
||||
- name: Run instrumentation tests
|
||||
uses: reactivecircus/android-emulator-runner@b530d96654c385303d652368551fb075bc2f0b6b # v2.35.0
|
||||
with:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
target: ${{ matrix.target }}
|
||||
force-avd-creation: false
|
||||
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-metrics
|
||||
disable-animations: true
|
||||
script: |
|
||||
adb wait-for-device
|
||||
./gradlew ${{ matrix.tasks }} -x lint --stacktrace && killall -INT crashpad_handler || true
|
||||
arch: x86_64
|
||||
profile: Nexus 6
|
||||
|
||||
2
.github/workflows/transifex.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v6
|
||||
- name: Run script to push English strings to Transifex
|
||||
run: |
|
||||
chmod +x ./scripts/push-to-transifex.sh
|
||||
|
||||
170
.gitignore
vendored
@@ -1,5 +1,167 @@
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/android,androidstudio
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=android,androidstudio
|
||||
|
||||
### Android ###
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Log/OS Files
|
||||
*.log
|
||||
|
||||
# Android Studio generated files and folders
|
||||
captures/
|
||||
.externalNativeBuild/
|
||||
.cxx/
|
||||
*.apk
|
||||
output.json
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.DS_Store
|
||||
.idea/
|
||||
misc.xml
|
||||
deploymentTargetDropDown.xml
|
||||
render.experimental.xml
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
google-services.json
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
### Android Patch ###
|
||||
gen-external-apklibs
|
||||
|
||||
# Replacement of .externalNativeBuild directories introduced
|
||||
# with Android Studio 3.5.
|
||||
|
||||
### AndroidStudio ###
|
||||
# Covers files to be ignored for android development using Android Studio.
|
||||
|
||||
# Built application files
|
||||
*.ap_
|
||||
*.aab
|
||||
|
||||
# Files for the ART/Dalvik VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
|
||||
# Gradle files
|
||||
.gradle
|
||||
|
||||
# Signing files
|
||||
.signing/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Log Files
|
||||
|
||||
# Android Studio
|
||||
/*/build/
|
||||
/*/local.properties
|
||||
/*/out
|
||||
/*/*/build
|
||||
/*/*/production
|
||||
.navigation/
|
||||
*.ipr
|
||||
*~
|
||||
*.swp
|
||||
|
||||
# Keystore files
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
# google-services.json
|
||||
|
||||
# Android Patch
|
||||
|
||||
# External native build folder generated in Android Studio 2.2 and later
|
||||
.externalNativeBuild
|
||||
|
||||
# NDK
|
||||
obj/
|
||||
|
||||
# IntelliJ IDEA
|
||||
*.iws
|
||||
/out/
|
||||
|
||||
# User-specific configurations
|
||||
.idea/caches/
|
||||
.idea/libraries/
|
||||
.idea/shelf/
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/.name
|
||||
.idea/compiler.xml
|
||||
.idea/copyright/profiles_settings.xml
|
||||
.idea/encodings.xml
|
||||
.idea/misc.xml
|
||||
.idea/modules.xml
|
||||
.idea/scopes/scope_settings.xml
|
||||
.idea/dictionaries
|
||||
.idea/vcs.xml
|
||||
.idea/jsLibraryMappings.xml
|
||||
.idea/datasources.xml
|
||||
.idea/dataSources.ids
|
||||
.idea/sqlDataSources.xml
|
||||
.idea/dynamic.xml
|
||||
.idea/uiDesigner.xml
|
||||
.idea/assetWizardSettings.xml
|
||||
.idea/gradle.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/navEditor.xml
|
||||
|
||||
# Legacy Eclipse project files
|
||||
.classpath
|
||||
.project
|
||||
.cproject
|
||||
.settings/
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.war
|
||||
*.ear
|
||||
|
||||
# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
|
||||
hs_err_pid*
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/mongoSettings.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
### AndroidStudio Patch ###
|
||||
|
||||
!/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/android,androidstudio
|
||||
2
BUILD.MD
@@ -47,6 +47,8 @@ To build the release build, run:
|
||||
|
||||
`gradlew assembleRelease`
|
||||
|
||||
We also need to add the changelog in a new file for the F-Droid release in [fastlane/metadata/android/en-US/changelogs](fastlane/metadata/android/en-US/changelogs), where the file name is the `version_code` (see [#559](https://github.com/barbeau/gpstest/issues/559)).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### When importing to Android Studio, I get an error "You are using an old, unsupported version of Gradle..."
|
||||
|
||||
2
FAQ.md
@@ -77,7 +77,7 @@ Android 1.5 and up, in its simplest form. More advanced versions with an update
|
||||
* **Start/Stop** - Start/stop the GPS hardware
|
||||
* **Send Location** - After a latitude and longitude has been acquired, you can share this info
|
||||
* **Inject Time** - Injects Time assistance data for GPS into the platform, using information from a [Network Time Protocol (NTP)](http://support.ntp.org/bin/view/Main/WebHome) server. Note that some devices don't use an NTP server for time data - if this is your device, you'll see a message saying "Platform does not support injecting time data".
|
||||
* **Inject PSDS Data** - Injects Predicted Satellite Data Service (PSDS) assistance data for GNSS into the platform, using information from a PSDS server. Note that some devices don't use PSDS for assistance data - if this is your device, you'll see a message saying "Platform does not support injecting PSDS data". PSDS is the generic term for products like [XTRA assistance data](http://goo.gl/3RjWX).
|
||||
* **Inject PSDS Data** - Injects Predicted Satellite Data Service (PSDS) assistance data for GNSS into the platform, using information from a PSDS server. Note that some devices don't use PSDS for assistance data - if this is your device, you'll see a message saying "Platform does not support injecting PSDS data". PSDS is the generic term for products like [XTRA assistance data](https://www.qualcomm.com/news/releases/2007/02/qualcomm-introduces-gpsonextra-assistance-expand-capabilities-standalone).
|
||||
* **Clear Aiding Data** - Clears all assistance data used for GPS, including NTP and PSDS/XTRA data (Note: if you select this option to fix broken GPS on your device, for GPS to work again you may need to ‘Inject Time’ and ‘Inject PSDS’ data). Note that some devices don't support clearing assistance data - if this is your device, you'll see a message saying "Platform does not support deleting aiding data". You may also see a large delay until your device acquires a fix again, so please use this feature with caution.
|
||||
* **Settings** - Set map tile type
|
||||
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'dagger.hilt.android.plugin'
|
||||
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
compileSdkVersion 33
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 18
|
||||
targetSdkVersion 29
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 34
|
||||
multiDexEnabled true
|
||||
// versionCode scheme - first two digits are minSdkVersion, last three digits are build number
|
||||
versionCode 18092
|
||||
versionName "3.9.15"
|
||||
versionCode 24099
|
||||
versionName "3.10.5"
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
// Enables Jetpack Compose for this module
|
||||
compose true
|
||||
}
|
||||
|
||||
flavorDimensions "map"
|
||||
|
||||
productFlavors {
|
||||
@@ -33,14 +41,23 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// http://stackoverflow.com/questions/20673625/gradle-0-7-0-duplicate-files-during-packaging-of-apk
|
||||
packagingOptions {
|
||||
// http://stackoverflow.com/questions/20673625/gradle-0-7-0-duplicate-files-during-packaging-of-apk
|
||||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/NOTICE'
|
||||
// https://github.com/Kotlin/kotlinx.coroutines/issues/2023
|
||||
// for JNA and JNA-platform
|
||||
exclude "META-INF/AL2.0"
|
||||
exclude "META-INF/LGPL2.1"
|
||||
// for byte-buddy
|
||||
exclude "META-INF/licenses/ASM"
|
||||
pickFirst "win32-x86-64/attach_hotspot_windows.dll"
|
||||
pickFirst "win32-x86/attach_hotspot_windows.dll"
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'MissingTranslation', 'ExtraTranslation'
|
||||
sarifReport true
|
||||
disable 'MissingTranslation', 'ExtraTranslation', 'StringFormatInvalid'
|
||||
}
|
||||
|
||||
if (project.hasProperty("secure.properties")
|
||||
@@ -89,13 +106,16 @@ android {
|
||||
}
|
||||
}
|
||||
}
|
||||
dataBinding {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
jvmTarget = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
testOptions {
|
||||
@@ -103,22 +123,36 @@ android {
|
||||
unitTests.includeAndroidResources true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion '1.4.0'
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding true
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
// Gradle automatically adds 'android.test.runner' as a dependency.
|
||||
useLibrary 'android.test.runner'
|
||||
|
||||
useLibrary 'android.test.base'
|
||||
useLibrary 'android.test.mock'
|
||||
namespace 'com.android.gpstest'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-rc1'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
|
||||
// ViewModel and LiveData
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:2.1.0-beta01"
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.6'
|
||||
|
||||
// Sliding drawer in map view
|
||||
implementation 'com.sothree.slidinguppanel:library:3.4.0'
|
||||
@@ -130,38 +164,74 @@ dependencies {
|
||||
implementation 'com.google.zxing:android-integration:3.3.0'
|
||||
|
||||
// Uploading device properties on user request
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
|
||||
implementation 'commons-io:commons-io:2.6'
|
||||
implementation 'androidx.core:core-ktx:1.10.0-rc01'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
|
||||
implementation 'commons-io:commons-io:2.14.0'
|
||||
|
||||
// Share dialog
|
||||
implementation 'androidx.viewpager2:viewpager2:1.0.0'
|
||||
|
||||
// Write GNSS antenna info to JSON
|
||||
implementation 'com.fasterxml.jackson.core:jackson-core:2.11.2'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.2'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-core:2.14.2'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.2'
|
||||
|
||||
// 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"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-service:$lifecycle_version"
|
||||
|
||||
// To use trySend() instead of offer() in callbackFlow (see https://github.com/Kotlin/kotlinx.coroutines/issues/974)
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1'
|
||||
|
||||
// Hilt for dependency injection
|
||||
implementation "com.google.dagger:hilt-android:$hilt_version"
|
||||
kapt "com.google.dagger:hilt-compiler:$hilt_version"
|
||||
|
||||
// Map (Google flavor only)
|
||||
googleImplementation 'com.google.android.gms:play-services-maps:16.1.0'
|
||||
googleImplementation 'com.google.android.gms:play-services-maps:17.0.1'
|
||||
// Calculating offset for camera target in map view (Google flavor only)
|
||||
googleImplementation 'com.google.maps.android:android-maps-utils:0.5'
|
||||
googleImplementation 'com.google.maps.android:android-maps-utils:2.2.6'
|
||||
// Use suspend coroutines instead of callbacks (Google flavor only)
|
||||
googleImplementation 'com.google.maps.android:maps-ktx:3.1.0'
|
||||
// OSM Droid (fdroid flavor only)
|
||||
osmdroidImplementation 'org.osmdroid:osmdroid-android:6.1.4'
|
||||
osmdroidImplementation 'org.osmdroid:osmdroid-android:6.1.11'
|
||||
|
||||
//
|
||||
// Jetpack Compose for UI
|
||||
//
|
||||
|
||||
// Integration with activities
|
||||
implementation 'androidx.activity:activity-compose:1.4.0'
|
||||
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
|
||||
implementation "com.google.android.material:compose-theme-adapter:1.1.0"
|
||||
// Animations
|
||||
implementation 'androidx.compose.animation:animation:1.1.0-beta02'
|
||||
// Tooling support (Previews, etc.)
|
||||
implementation 'androidx.compose.ui:ui-tooling:1.0.5'
|
||||
// Integration with ViewModels
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'
|
||||
//Integration with LiveData
|
||||
implementation 'androidx.compose.runtime:runtime-livedata:1.1.0-beta02'
|
||||
// UI Tests
|
||||
androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.0.5'
|
||||
|
||||
//
|
||||
// Unit tests
|
||||
//
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
// Core library
|
||||
androidTestImplementation 'androidx.test:core:1.3.0'
|
||||
androidTestImplementation 'androidx.test:core:1.4.0'
|
||||
|
||||
// AndroidJUnitRunner and JUnit Rules
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
||||
|
||||
// Assertions
|
||||
androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
|
||||
|
||||
@@ -28,4 +28,8 @@
|
||||
# for SlidingUpPanelLayout and compiling with API 28 (see https://github.com/barbeau/gpstest/issues/273)
|
||||
-dontwarn com.sothree.**
|
||||
-keep class com.sothree.**
|
||||
-keep interface com.sothree.**
|
||||
-keep interface com.sothree.**
|
||||
|
||||
# for Jackon and outputting AntennaInfo
|
||||
-dontwarn java.beans.ConstructorProperties
|
||||
-dontwarn java.beans.Transient
|
||||
@@ -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
|
||||
@@ -86,7 +86,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
qzssL1.hasCarrierFrequency = true
|
||||
qzssL1.carrierFrequencyHz = 1575420000.0f
|
||||
qzssL1.carrierFrequencyHz = 1575420000.0
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(qzssL1)
|
||||
assertEquals("L1", label)
|
||||
@@ -101,7 +101,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
qzssL2.hasCarrierFrequency = true
|
||||
qzssL2.carrierFrequencyHz = 1227600000.0f
|
||||
qzssL2.carrierFrequencyHz = 1227600000.0
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(qzssL2)
|
||||
assertEquals("L2", label)
|
||||
@@ -116,7 +116,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
qzssL5.hasCarrierFrequency = true
|
||||
qzssL5.carrierFrequencyHz = 1176450000.0f
|
||||
qzssL5.carrierFrequencyHz = 1176450000.0
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(qzssL5)
|
||||
assertEquals("L5", label)
|
||||
@@ -131,7 +131,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
qzssL6.hasCarrierFrequency = true
|
||||
qzssL6.carrierFrequencyHz = 1278750000.0f
|
||||
qzssL6.carrierFrequencyHz = 1278750000.0
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(qzssL6)
|
||||
assertEquals("L6", label)
|
||||
@@ -150,7 +150,7 @@ class CarrierFreqUtilsTest {
|
||||
|
||||
// Beidou
|
||||
|
||||
// Beidou B1
|
||||
// Beidou B1I
|
||||
val beidouB1 = SatelliteStatus(1,
|
||||
GnssType.BEIDOU,
|
||||
30f,
|
||||
@@ -160,25 +160,10 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
beidouB1.hasCarrierFrequency = true
|
||||
beidouB1.carrierFrequencyHz = 1561098000.0f
|
||||
beidouB1.carrierFrequencyHz = 1561098000.0
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(beidouB1)
|
||||
assertEquals("B1", label)
|
||||
|
||||
// Beidou B1-2
|
||||
val beidouB1_2 = SatelliteStatus(1,
|
||||
GnssType.BEIDOU,
|
||||
30f,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
72f,
|
||||
25f);
|
||||
beidouB1_2.hasCarrierFrequency = true
|
||||
beidouB1_2.carrierFrequencyHz = 1589742000.0f
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(beidouB1_2)
|
||||
assertEquals("B1-2", label)
|
||||
assertEquals("B1I", label)
|
||||
|
||||
// Beidou B1C
|
||||
val beidouB1c = SatelliteStatus(1,
|
||||
@@ -190,7 +175,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
beidouB1c.hasCarrierFrequency = true
|
||||
beidouB1c.carrierFrequencyHz = 1575420000.0f
|
||||
beidouB1c.carrierFrequencyHz = 1575420000.0
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(beidouB1c)
|
||||
assertEquals("B1C", label)
|
||||
@@ -205,26 +190,11 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
beidouB1c202.hasCarrierFrequency = true
|
||||
beidouB1c202.carrierFrequencyHz = 1575450000.0f
|
||||
beidouB1c202.carrierFrequencyHz = 1575450000.0
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(beidouB1c202)
|
||||
assertEquals("B1C", label)
|
||||
|
||||
// Beidou B2
|
||||
val beidouB2 = SatelliteStatus(1,
|
||||
GnssType.BEIDOU,
|
||||
30f,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
72f,
|
||||
25f);
|
||||
beidouB2.hasCarrierFrequency = true
|
||||
beidouB2.carrierFrequencyHz = 1207140000.0f
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(beidouB2)
|
||||
assertEquals("B2", label)
|
||||
|
||||
// Beidou B2a
|
||||
val beidouB2a = SatelliteStatus(1,
|
||||
GnssType.BEIDOU,
|
||||
@@ -235,12 +205,28 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
beidouB2a.hasCarrierFrequency = true
|
||||
beidouB2a.carrierFrequencyHz = 1176450000.0f
|
||||
beidouB2a.carrierFrequencyHz = 1176450000.0
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(beidouB2a)
|
||||
assertEquals("B2a", label)
|
||||
|
||||
// Beidou B3
|
||||
|
||||
// Beidou B2b
|
||||
val beidouB2b = SatelliteStatus(1,
|
||||
GnssType.BEIDOU,
|
||||
30f,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
72f,
|
||||
25f);
|
||||
beidouB2b.hasCarrierFrequency = true
|
||||
beidouB2b.carrierFrequencyHz = 1207140000.0
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(beidouB2b)
|
||||
assertEquals("B2b", label)
|
||||
|
||||
// Beidou B3I
|
||||
val beidouB3 = SatelliteStatus(1,
|
||||
GnssType.BEIDOU,
|
||||
30f,
|
||||
@@ -250,13 +236,28 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
beidouB3.hasCarrierFrequency = true
|
||||
beidouB3.carrierFrequencyHz = 1268520000.0f
|
||||
beidouB3.carrierFrequencyHz = 1268520000.0
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(beidouB3)
|
||||
assertEquals("B3", label)
|
||||
assertEquals("B3I", label)
|
||||
|
||||
// IRNSS
|
||||
|
||||
// IRNSS L1
|
||||
val irnssL1 = SatelliteStatus(1,
|
||||
GnssType.IRNSS,
|
||||
30f,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
72f,
|
||||
25f);
|
||||
irnssL1.hasCarrierFrequency = true
|
||||
irnssL1.carrierFrequencyHz = 1575420000.0
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(irnssL1)
|
||||
assertEquals("L1", label)
|
||||
|
||||
// IRNSS L5
|
||||
val irnssL5 = SatelliteStatus(1,
|
||||
GnssType.IRNSS,
|
||||
@@ -267,7 +268,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
irnssL5.hasCarrierFrequency = true
|
||||
irnssL5.carrierFrequencyHz = 1176450000.0f
|
||||
irnssL5.carrierFrequencyHz = 1176450000.0
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(irnssL5)
|
||||
assertEquals("L5", label)
|
||||
@@ -282,7 +283,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
irnssS.hasCarrierFrequency = true
|
||||
irnssS.carrierFrequencyHz = 2492028000.0f
|
||||
irnssS.carrierFrequencyHz = 2492028000.0
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(irnssS)
|
||||
assertEquals("S", label)
|
||||
@@ -297,26 +298,26 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
gagan.hasCarrierFrequency = true
|
||||
gagan.carrierFrequencyHz = 1575420000.0f
|
||||
gagan.carrierFrequencyHz = 1575420000.0
|
||||
gagan.sbasType = SbasType.GAGAN
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(gagan)
|
||||
assertEquals("L1", label)
|
||||
|
||||
// EGNOS - ID 120 L1
|
||||
val egnos120 = SatelliteStatus(120,
|
||||
GnssType.SBAS,
|
||||
30f,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
72f,
|
||||
25f);
|
||||
egnos120.hasCarrierFrequency = true
|
||||
egnos120.carrierFrequencyHz = 1575420000.0f
|
||||
egnos120.sbasType = SbasType.EGNOS
|
||||
// EGNOS - ID 121 L1
|
||||
val egnos121 = SatelliteStatus(121,
|
||||
GnssType.SBAS,
|
||||
30f,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
72f,
|
||||
25f);
|
||||
egnos121.hasCarrierFrequency = true
|
||||
egnos121.carrierFrequencyHz = 1575420000.0
|
||||
egnos121.sbasType = SbasType.EGNOS
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(egnos120)
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(egnos121)
|
||||
assertEquals("L1", label)
|
||||
|
||||
// EGNOS - ID 123 L1
|
||||
@@ -329,7 +330,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
egnos123.hasCarrierFrequency = true
|
||||
egnos123.carrierFrequencyHz = 1575420000.0f
|
||||
egnos123.carrierFrequencyHz = 1575420000.0
|
||||
egnos123.sbasType = SbasType.EGNOS
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(egnos123)
|
||||
@@ -345,7 +346,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
egnos126.hasCarrierFrequency = true
|
||||
egnos126.carrierFrequencyHz = 1575420000.0f
|
||||
egnos126.carrierFrequencyHz = 1575420000.0
|
||||
egnos126.sbasType = SbasType.EGNOS
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(egnos126)
|
||||
@@ -361,26 +362,26 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
egnos136.hasCarrierFrequency = true
|
||||
egnos136.carrierFrequencyHz = 1575420000.0f
|
||||
egnos136.carrierFrequencyHz = 1575420000.0
|
||||
egnos136.sbasType = SbasType.EGNOS
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(egnos136)
|
||||
assertEquals("L1", label)
|
||||
|
||||
// EGNOS - ID 120 L5
|
||||
val egnos120L5 = SatelliteStatus(120,
|
||||
GnssType.SBAS,
|
||||
30f,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
72f,
|
||||
25f);
|
||||
egnos120L5.hasCarrierFrequency = true
|
||||
egnos120L5.carrierFrequencyHz = 1176450000.0f
|
||||
egnos120L5.sbasType = SbasType.EGNOS
|
||||
// EGNOS - ID 121 L5
|
||||
val egnos121L5 = SatelliteStatus(121,
|
||||
GnssType.SBAS,
|
||||
30f,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
72f,
|
||||
25f);
|
||||
egnos121L5.hasCarrierFrequency = true
|
||||
egnos121L5.carrierFrequencyHz = 1176450000.0
|
||||
egnos121L5.sbasType = SbasType.EGNOS
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(egnos120L5)
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(egnos121L5)
|
||||
assertEquals("L5", label)
|
||||
|
||||
// EGNOS - ID 123 L5
|
||||
@@ -393,7 +394,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
egnos123L5.hasCarrierFrequency = true
|
||||
egnos123L5.carrierFrequencyHz = 1176450000.0f
|
||||
egnos123L5.carrierFrequencyHz = 1176450000.0
|
||||
egnos123L5.sbasType = SbasType.EGNOS
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(egnos123L5)
|
||||
@@ -409,7 +410,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
egnos126L5.hasCarrierFrequency = true
|
||||
egnos126L5.carrierFrequencyHz = 1176450000.0f
|
||||
egnos126L5.carrierFrequencyHz = 1176450000.0
|
||||
egnos126L5.sbasType = SbasType.EGNOS
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(egnos126L5)
|
||||
@@ -425,7 +426,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
egnos136L5.hasCarrierFrequency = true
|
||||
egnos136L5.carrierFrequencyHz = 1176450000.0f
|
||||
egnos136L5.carrierFrequencyHz = 1176450000.0
|
||||
egnos136L5.sbasType = SbasType.EGNOS
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(egnos136L5)
|
||||
@@ -443,7 +444,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
egnos133L1.hasCarrierFrequency = true
|
||||
egnos133L1.carrierFrequencyHz = 1575420000.0f
|
||||
egnos133L1.carrierFrequencyHz = 1575420000.0
|
||||
egnos133L1.sbasType = SbasType.EGNOS
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(egnos133L1)
|
||||
@@ -459,7 +460,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
egnos133L5.hasCarrierFrequency = true
|
||||
egnos133L5.carrierFrequencyHz = 1176450000.0f
|
||||
egnos133L5.carrierFrequencyHz = 1176450000.0
|
||||
egnos133L5.sbasType = SbasType.EGNOS
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(egnos133L5)
|
||||
@@ -493,7 +494,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
anik15_138L1.hasCarrierFrequency = true
|
||||
anik15_138L1.carrierFrequencyHz = 1575420000.0f
|
||||
anik15_138L1.carrierFrequencyHz = 1575420000.0
|
||||
anik15_138L1.sbasType = SbasType.WAAS
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(anik15_138L1)
|
||||
@@ -509,7 +510,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
anik15_138L5.hasCarrierFrequency = true
|
||||
anik15_138L5.carrierFrequencyHz = 1176450000.0f
|
||||
anik15_138L5.carrierFrequencyHz = 1176450000.0
|
||||
anik15_138L5.sbasType = SbasType.WAAS
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(anik15_138L5)
|
||||
@@ -525,7 +526,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
ses5_136L1.hasCarrierFrequency = true
|
||||
ses5_136L1.carrierFrequencyHz = 1575420000.0f
|
||||
ses5_136L1.carrierFrequencyHz = 1575420000.0
|
||||
ses5_136L1.sbasType = SbasType.EGNOS
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(ses5_136L1)
|
||||
@@ -541,7 +542,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
ses5_136L5.hasCarrierFrequency = true
|
||||
ses5_136L5.carrierFrequencyHz = 1176450000.0f
|
||||
ses5_136L5.carrierFrequencyHz = 1176450000.0
|
||||
ses5_136L5.sbasType = SbasType.EGNOS
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(ses5_136L5)
|
||||
@@ -559,7 +560,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
msas129L1.hasCarrierFrequency = true
|
||||
msas129L1.carrierFrequencyHz = 1575420000.0f
|
||||
msas129L1.carrierFrequencyHz = 1575420000.0
|
||||
msas129L1.sbasType = SbasType.MSAS
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(msas129L1)
|
||||
@@ -575,7 +576,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
msas129L5.hasCarrierFrequency = true
|
||||
msas129L5.carrierFrequencyHz = 1176450000.0f
|
||||
msas129L5.carrierFrequencyHz = 1176450000.0
|
||||
msas129L5.sbasType = SbasType.MSAS
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(msas129L5)
|
||||
@@ -591,7 +592,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
msas137L1.hasCarrierFrequency = true
|
||||
msas137L1.carrierFrequencyHz = 1575420000.0f
|
||||
msas137L1.carrierFrequencyHz = 1575420000.0
|
||||
msas137L1.sbasType = SbasType.MSAS
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(msas137L1)
|
||||
@@ -607,12 +608,79 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
msas137L5.hasCarrierFrequency = true
|
||||
msas137L5.carrierFrequencyHz = 1176450000.0f
|
||||
msas137L5.carrierFrequencyHz = 1176450000.0
|
||||
msas137L5.sbasType = SbasType.MSAS
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(msas137L5)
|
||||
assertEquals("L5", label)
|
||||
|
||||
// SDCM L1 - 125
|
||||
val sdcm125L1 = SatelliteStatus(125,
|
||||
GnssType.SBAS,
|
||||
30f,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
72f,
|
||||
25f);
|
||||
sdcm125L1.hasCarrierFrequency = true
|
||||
sdcm125L1.carrierFrequencyHz = 1575420000.0
|
||||
sdcm125L1.sbasType = SbasType.SDCM
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(sdcm125L1)
|
||||
assertEquals("L1-C", label)
|
||||
|
||||
// SDCM L5 - 141
|
||||
val sdcm125L5 = SatelliteStatus(141,
|
||||
GnssType.SBAS,
|
||||
30f,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
72f,
|
||||
25f);
|
||||
sdcm125L5.hasCarrierFrequency = true
|
||||
sdcm125L5.carrierFrequencyHz = 1176450000.0
|
||||
sdcm125L5.sbasType = SbasType.SDCM
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(sdcm125L5)
|
||||
assertEquals("L5", label)
|
||||
|
||||
// INMARSAT_4F1 (SBAS)
|
||||
|
||||
// SouthPAN - ID 122 L1
|
||||
val southpan122L1 = SatelliteStatus(122,
|
||||
GnssType.SBAS,
|
||||
30f,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
72f,
|
||||
25f);
|
||||
southpan122L1.hasCarrierFrequency = true
|
||||
southpan122L1.carrierFrequencyHz = 1575420000.0
|
||||
southpan122L1.sbasType = SbasType.SOUTHPAN
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(southpan122L1)
|
||||
assertEquals("L1", label)
|
||||
|
||||
// SouthPAN - ID 122 L5
|
||||
val southpan122L5 = SatelliteStatus(122,
|
||||
GnssType.SBAS,
|
||||
30f,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
72f,
|
||||
25f);
|
||||
southpan122L5.hasCarrierFrequency = true
|
||||
southpan122L5.carrierFrequencyHz = 1176450000.0
|
||||
southpan122L5.sbasType = SbasType.SOUTHPAN
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(southpan122L5)
|
||||
assertEquals("L5", label)
|
||||
|
||||
|
||||
// Test variations on the "same" numbers to make sure floating point equality works
|
||||
val gpsL1variation = SatelliteStatus(1,
|
||||
GnssType.NAVSTAR,
|
||||
@@ -623,7 +691,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
gpsL1variation.hasCarrierFrequency = true
|
||||
gpsL1variation.carrierFrequencyHz = 1575420000.0000000f
|
||||
gpsL1variation.carrierFrequencyHz = 1575420000.0000000
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(gpsL1variation)
|
||||
assertEquals("L1", label)
|
||||
@@ -651,7 +719,7 @@ class CarrierFreqUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
gpsL1badCf.hasCarrierFrequency = true
|
||||
gpsL1badCf.carrierFrequencyHz = 12345.0f
|
||||
gpsL1badCf.carrierFrequencyHz = 12345.0
|
||||
|
||||
label = CarrierFreqUtils.getCarrierFrequencyLabel(gpsL1badCf)
|
||||
assertEquals(CF_UNKNOWN, label)
|
||||
@@ -665,7 +733,7 @@ class CarrierFreqUtilsTest {
|
||||
fun testIsPrimaryCarrier() {
|
||||
assertTrue(CarrierFreqUtils.isPrimaryCarrier("L1"))
|
||||
assertTrue(CarrierFreqUtils.isPrimaryCarrier("E1"))
|
||||
assertTrue(CarrierFreqUtils.isPrimaryCarrier("B1"))
|
||||
assertTrue(CarrierFreqUtils.isPrimaryCarrier("B1I"))
|
||||
assertTrue(CarrierFreqUtils.isPrimaryCarrier("B1C"))
|
||||
assertTrue(CarrierFreqUtils.isPrimaryCarrier("L1-C"))
|
||||
|
||||
|
||||
@@ -1,457 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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.
|
||||
*/
|
||||
|
||||
package com.android.gpstest
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Build
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import androidx.test.InstrumentationRegistry
|
||||
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
|
||||
import com.android.gpstest.model.GnssType
|
||||
import com.android.gpstest.model.SbasType
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4ClassRunner::class)
|
||||
class DeviceInfoViewModelTest {
|
||||
|
||||
// Required to allow LiveData to execute
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
/**
|
||||
* Test aggregating signal information into satellites
|
||||
*/
|
||||
@Test
|
||||
fun testDeviceInfoViewModel() {
|
||||
val modelNull = DeviceInfoViewModel(InstrumentationRegistry.getTargetContext().applicationContext as Application)
|
||||
modelNull.setStatuses(null, null)
|
||||
|
||||
// Test GPS L1 - should be 1 satellite, no L5 or dual-frequency
|
||||
val modelGpsL1 = DeviceInfoViewModel(InstrumentationRegistry.getTargetContext().applicationContext as Application)
|
||||
modelGpsL1.setStatuses(listOf(gpsL1(1, true)), null)
|
||||
assertEquals(1, modelGpsL1.gnssSatellites.value?.size)
|
||||
assertFalse(modelGpsL1.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGpsL1.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL1.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1.isDualFrequencyPerSatInUse)
|
||||
assertEquals(1, modelGpsL1.satelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(1, modelGpsL1.satelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelGpsL1.satelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(1, modelGpsL1.satelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(1, modelGpsL1.satelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(1, modelGpsL1.satelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(1, modelGpsL1.supportedGnss.size)
|
||||
assertEquals(0, modelGpsL1.supportedSbas.size)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertEquals(1, modelGpsL1.supportedGnssCfs.size)
|
||||
assertTrue(modelGpsL1.supportedGnssCfs.contains("L1"))
|
||||
} else {
|
||||
assertEquals(0, modelGpsL1.supportedGnssCfs.size)
|
||||
}
|
||||
assertEquals(0, modelGpsL1.supportedSbasCfs.size)
|
||||
assertTrue(modelGpsL1.supportedGnss.contains(GnssType.NAVSTAR))
|
||||
|
||||
modelGpsL1.reset();
|
||||
|
||||
// Test GPS L1 no signal - should be 1 satellite, no L5 or dual-frequency
|
||||
modelGpsL1.setStatuses(listOf(gpsL1NoSignal(1)), null)
|
||||
assertEquals(1, modelGpsL1.gnssSatellites.value?.size)
|
||||
assertFalse(modelGpsL1.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGpsL1.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL1.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1.isDualFrequencyPerSatInUse)
|
||||
assertEquals(0, modelGpsL1.satelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(0, modelGpsL1.satelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelGpsL1.satelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(0, modelGpsL1.satelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(0, modelGpsL1.satelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(1, modelGpsL1.satelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(1, modelGpsL1.supportedGnss.size)
|
||||
assertEquals(0, modelGpsL1.supportedSbas.size)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertEquals(1, modelGpsL1.supportedGnssCfs.size)
|
||||
assertTrue(modelGpsL1.supportedGnssCfs.contains("L1"))
|
||||
} else {
|
||||
assertEquals(0, modelGpsL1.supportedGnssCfs.size)
|
||||
}
|
||||
assertEquals(0, modelGpsL1.supportedSbasCfs.size)
|
||||
assertTrue(modelGpsL1.supportedGnss.contains(GnssType.NAVSTAR))
|
||||
|
||||
|
||||
// Test GPS L1 + L5 same sv - should be 1 satellite, dual frequency in view and but not in use
|
||||
val modelGpsL1L5 = DeviceInfoViewModel(InstrumentationRegistry.getTargetContext().applicationContext as Application)
|
||||
modelGpsL1L5.setStatuses(listOf(gpsL1(1, false), gpsL5(1, true)), null)
|
||||
assertEquals(1, modelGpsL1L5.gnssSatellites.value?.size)
|
||||
assertEquals(1, modelGpsL1L5.supportedGnss.size)
|
||||
assertEquals(0, modelGpsL1L5.supportedSbas.size)
|
||||
assertEquals(0, modelGpsL1L5.supportedSbasCfs.size)
|
||||
assertTrue(modelGpsL1L5.supportedGnss.contains(GnssType.NAVSTAR))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertTrue(modelGpsL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertTrue(modelGpsL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertTrue(modelGpsL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInUse)
|
||||
assertEquals(1, modelGpsL1L5.satelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(1, modelGpsL1L5.satelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelGpsL1L5.satelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(2, modelGpsL1L5.satelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(1, modelGpsL1L5.satelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelGpsL1L5.satelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(2, modelGpsL1L5.supportedGnssCfs.size)
|
||||
assertTrue(modelGpsL1L5.supportedGnssCfs.contains("L1"))
|
||||
assertTrue(modelGpsL1L5.supportedGnssCfs.contains("L5"))
|
||||
} else {
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInUse)
|
||||
// Because carrier frequency isn't considered, these signals should be detected as duplicates
|
||||
assertEquals(1, modelGpsL1L5.duplicateCarrierStatuses.size)
|
||||
assertEquals(0, modelGpsL1L5.supportedGnssCfs.size)
|
||||
}
|
||||
|
||||
modelGpsL1L5.reset();
|
||||
|
||||
// Test GPS L1 + L5 same sv - should be 1 satellite, dual-frequency in view and use
|
||||
modelGpsL1L5.setStatuses(listOf(gpsL1(1, true), gpsL5(1, true)), null)
|
||||
assertEquals(1, modelGpsL1L5.gnssSatellites.value?.size)
|
||||
assertEquals(1, modelGpsL1L5.supportedGnss.size)
|
||||
assertEquals(0, modelGpsL1L5.supportedSbas.size)
|
||||
assertEquals(0, modelGpsL1L5.supportedSbasCfs.size)
|
||||
assertTrue(modelGpsL1L5.supportedGnss.contains(GnssType.NAVSTAR))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertTrue(modelGpsL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertTrue(modelGpsL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertTrue(modelGpsL1L5.isDualFrequencyPerSatInView)
|
||||
assertTrue(modelGpsL1L5.isDualFrequencyPerSatInUse)
|
||||
assertEquals(1, modelGpsL1L5.satelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(1, modelGpsL1L5.satelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelGpsL1L5.satelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(2, modelGpsL1L5.satelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(2, modelGpsL1L5.satelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelGpsL1L5.satelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(2, modelGpsL1L5.supportedGnssCfs.size)
|
||||
assertTrue(modelGpsL1L5.supportedGnssCfs.contains("L1"))
|
||||
assertTrue(modelGpsL1L5.supportedGnssCfs.contains("L5"))
|
||||
} else {
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInUse)
|
||||
// Because carrier frequency isn't considered, these signals should be detected as duplicates
|
||||
assertEquals(1, modelGpsL1L5.duplicateCarrierStatuses.size)
|
||||
assertEquals(0, modelGpsL1L5.supportedGnssCfs.size)
|
||||
}
|
||||
|
||||
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.setStatuses(listOf(gpsL1(1, true), gpsL5(1, false)), null)
|
||||
assertEquals(1, modelGpsL1L5.gnssSatellites.value?.size)
|
||||
assertEquals(1, modelGpsL1L5.supportedGnss.size)
|
||||
assertEquals(0, modelGpsL1L5.supportedSbas.size)
|
||||
assertEquals(0, modelGpsL1L5.supportedSbasCfs.size)
|
||||
assertTrue(modelGpsL1L5.supportedGnss.contains(GnssType.NAVSTAR))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertTrue(modelGpsL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertTrue(modelGpsL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInUse)
|
||||
assertEquals(1, modelGpsL1L5.satelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(1, modelGpsL1L5.satelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelGpsL1L5.satelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(2, modelGpsL1L5.satelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(1, modelGpsL1L5.satelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelGpsL1L5.satelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(2, modelGpsL1L5.supportedGnssCfs.size)
|
||||
assertTrue(modelGpsL1L5.supportedGnssCfs.contains("L1"))
|
||||
assertTrue(modelGpsL1L5.supportedGnssCfs.contains("L5"))
|
||||
} else {
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInUse)
|
||||
// Because carrier frequency isn't considered, these signals should be detected as duplicates
|
||||
assertEquals(1, modelGpsL1L5.duplicateCarrierStatuses.size)
|
||||
assertEquals(0, modelGpsL1L5.supportedGnssCfs.size)
|
||||
}
|
||||
|
||||
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.setStatuses(listOf(gpsL1(1, true), gpsL5(2, true)), null)
|
||||
assertEquals(2, modelGpsL1L5.gnssSatellites.value?.size)
|
||||
assertEquals(1, modelGpsL1L5.supportedGnss.size)
|
||||
assertEquals(0, modelGpsL1L5.supportedSbas.size)
|
||||
assertEquals(0, modelGpsL1L5.supportedSbasCfs.size)
|
||||
assertTrue(modelGpsL1L5.supportedGnss.contains(GnssType.NAVSTAR))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertTrue(modelGpsL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertTrue(modelGpsL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInUse)
|
||||
assertEquals(2, modelGpsL1L5.satelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(2, modelGpsL1L5.satelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(2, modelGpsL1L5.satelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(2, modelGpsL1L5.satelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(2, modelGpsL1L5.satelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelGpsL1L5.satelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(2, modelGpsL1L5.supportedGnssCfs.size)
|
||||
assertTrue(modelGpsL1L5.supportedGnssCfs.contains("L1"))
|
||||
assertTrue(modelGpsL1L5.supportedGnssCfs.contains("L5"))
|
||||
} else {
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInUse)
|
||||
assertEquals(2, modelGpsL1L5.satelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(2, modelGpsL1L5.satelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(2, modelGpsL1L5.satelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(2, modelGpsL1L5.satelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(2, modelGpsL1L5.satelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelGpsL1L5.satelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(0, modelGpsL1L5.supportedGnssCfs.size)
|
||||
}
|
||||
|
||||
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.setStatuses(listOf(gpsL1NoSignal(1), gpsL5(1, true)), null)
|
||||
assertEquals(1, modelGpsL1L5.gnssSatellites.value?.size)
|
||||
assertEquals(1, modelGpsL1L5.supportedGnss.size)
|
||||
assertEquals(0, modelGpsL1L5.supportedSbas.size)
|
||||
assertEquals(0, modelGpsL1L5.supportedSbasCfs.size)
|
||||
assertTrue(modelGpsL1L5.supportedGnss.contains(GnssType.NAVSTAR))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertTrue(modelGpsL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertTrue(modelGpsL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInUse)
|
||||
assertEquals(1, modelGpsL1L5.satelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(1, modelGpsL1L5.satelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelGpsL1L5.satelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(1, modelGpsL1L5.satelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(1, modelGpsL1L5.satelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelGpsL1L5.satelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(2, modelGpsL1L5.supportedGnssCfs.size)
|
||||
assertTrue(modelGpsL1L5.supportedGnssCfs.contains("L1"))
|
||||
assertTrue(modelGpsL1L5.supportedGnssCfs.contains("L5"))
|
||||
} else {
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInUse)
|
||||
// Because carrier frequency isn't considered, these signals should be detected as duplicates
|
||||
assertEquals(1, modelGpsL1L5.duplicateCarrierStatuses.size)
|
||||
assertEquals(0, modelGpsL1L5.supportedGnssCfs.size)
|
||||
}
|
||||
|
||||
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 = DeviceInfoViewModel(InstrumentationRegistry.getTargetContext().applicationContext as Application)
|
||||
modelGpsL5.setStatuses(listOf(gpsL5(1, false)), null)
|
||||
assertEquals(1, modelGpsL5.gnssSatellites.value?.size)
|
||||
assertEquals(1, modelGpsL5.supportedGnss.size)
|
||||
assertEquals(0, modelGpsL5.supportedSbas.size)
|
||||
assertEquals(0, modelGpsL5.supportedSbasCfs.size)
|
||||
assertTrue(modelGpsL5.supportedGnss.contains(GnssType.NAVSTAR))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertTrue(modelGpsL5.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGpsL5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL5.isDualFrequencyPerSatInUse)
|
||||
assertEquals(1, modelGpsL5.satelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(0, modelGpsL5.satelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelGpsL5.satelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(1, modelGpsL5.satelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(0, modelGpsL5.satelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(1, modelGpsL5.satelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(1, modelGpsL5.supportedGnssCfs.size)
|
||||
assertTrue(modelGpsL5.supportedGnssCfs.contains("L5"))
|
||||
} else {
|
||||
assertFalse(modelGpsL5.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGpsL5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL5.isDualFrequencyPerSatInUse)
|
||||
assertEquals(1, modelGpsL5.satelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(0, modelGpsL5.satelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelGpsL5.satelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(1, modelGpsL5.satelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(0, modelGpsL5.satelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(1, modelGpsL5.satelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(0, modelGpsL5.supportedGnssCfs.size)
|
||||
}
|
||||
|
||||
// Test GPS L1 + GLONASS L1 - should be 2 satellites, no non-primary carrier of dual-freq
|
||||
val modelGpsL1GlonassL1 = DeviceInfoViewModel(InstrumentationRegistry.getTargetContext().applicationContext as Application)
|
||||
modelGpsL1GlonassL1.setStatuses(listOf(gpsL1(1, true), glonassL1variant1()), null)
|
||||
assertEquals(2, modelGpsL1GlonassL1.gnssSatellites.value?.size)
|
||||
assertFalse(modelGpsL1GlonassL1.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGpsL1GlonassL1.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL1GlonassL1.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1GlonassL1.isDualFrequencyPerSatInUse)
|
||||
assertEquals(2, modelGpsL1GlonassL1.satelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(2, modelGpsL1GlonassL1.satelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(2, modelGpsL1GlonassL1.satelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(2, modelGpsL1GlonassL1.satelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(2, modelGpsL1GlonassL1.satelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelGpsL1GlonassL1.satelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(2, modelGpsL1GlonassL1.supportedGnss.size)
|
||||
assertEquals(0, modelGpsL1GlonassL1.supportedSbas.size)
|
||||
assertEquals(0, modelGpsL1GlonassL1.supportedSbasCfs.size)
|
||||
assertTrue(modelGpsL1GlonassL1.supportedGnss.contains(GnssType.NAVSTAR))
|
||||
assertTrue(modelGpsL1GlonassL1.supportedGnss.contains(GnssType.GLONASS))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertEquals(1, modelGpsL1GlonassL1.supportedGnssCfs.size)
|
||||
assertTrue(modelGpsL1GlonassL1.supportedGnssCfs.contains("L1"))
|
||||
} else {
|
||||
assertEquals(0, modelGpsL1GlonassL1.supportedGnssCfs.size)
|
||||
}
|
||||
|
||||
// Test Galileo E1 + E5a - should be 2 satellites, dual frequency not in use, non-primary carrier of dual-freq
|
||||
val modelGalileoE1E5a = DeviceInfoViewModel(InstrumentationRegistry.getTargetContext().applicationContext as Application)
|
||||
modelGalileoE1E5a.setStatuses(listOf(galileoE1(1, true), galileoE5a(2, true)), null)
|
||||
assertEquals(2, modelGalileoE1E5a.gnssSatellites.value?.size)
|
||||
assertEquals(1, modelGalileoE1E5a.supportedGnss.size)
|
||||
assertEquals(0, modelGalileoE1E5a.supportedSbas.size)
|
||||
assertEquals(0, modelGalileoE1E5a.supportedSbasCfs.size)
|
||||
assertTrue(modelGalileoE1E5a.supportedGnss.contains(GnssType.GALILEO))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertTrue(modelGalileoE1E5a.isNonPrimaryCarrierFreqInView)
|
||||
assertTrue(modelGalileoE1E5a.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGalileoE1E5a.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGalileoE1E5a.isDualFrequencyPerSatInUse)
|
||||
assertEquals(2, modelGalileoE1E5a.satelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(2, modelGalileoE1E5a.satelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(2, modelGalileoE1E5a.satelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(2, modelGalileoE1E5a.satelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(2, modelGalileoE1E5a.satelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelGalileoE1E5a.satelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(2, modelGalileoE1E5a.supportedGnssCfs.size)
|
||||
assertTrue(modelGalileoE1E5a.supportedGnssCfs.contains("E1"))
|
||||
assertTrue(modelGalileoE1E5a.supportedGnssCfs.contains("E5a"))
|
||||
} else {
|
||||
assertFalse(modelGalileoE1E5a.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGalileoE1E5a.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGalileoE1E5a.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGalileoE1E5a.isDualFrequencyPerSatInUse)
|
||||
assertEquals(2, modelGalileoE1E5a.satelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(2, modelGalileoE1E5a.satelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(2, modelGalileoE1E5a.satelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(2, modelGalileoE1E5a.satelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(2, modelGalileoE1E5a.satelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelGalileoE1E5a.satelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(0, modelGalileoE1E5a.supportedGnssCfs.size)
|
||||
}
|
||||
|
||||
modelGalileoE1E5a.reset()
|
||||
|
||||
// Test Galileo E1 + E5a - should be 1 satellites, dual frequency in use, non-primary carrier of dual-freq
|
||||
modelGalileoE1E5a.setStatuses(listOf(galileoE1(1, true), galileoE5a(1, true)), null)
|
||||
assertEquals(1, modelGalileoE1E5a.gnssSatellites.value?.size)
|
||||
assertEquals(1, modelGalileoE1E5a.supportedGnss.size)
|
||||
assertEquals(0, modelGalileoE1E5a.supportedSbas.size)
|
||||
assertEquals(0, modelGalileoE1E5a.supportedSbasCfs.size)
|
||||
assertTrue(modelGalileoE1E5a.supportedGnss.contains(GnssType.GALILEO))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertTrue(modelGalileoE1E5a.isNonPrimaryCarrierFreqInView)
|
||||
assertTrue(modelGalileoE1E5a.isNonPrimaryCarrierFreqInUse)
|
||||
assertTrue(modelGalileoE1E5a.isDualFrequencyPerSatInView)
|
||||
assertTrue(modelGalileoE1E5a.isDualFrequencyPerSatInUse)
|
||||
assertEquals(1, modelGalileoE1E5a.satelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(1, modelGalileoE1E5a.satelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelGalileoE1E5a.satelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(2, modelGalileoE1E5a.satelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(2, modelGalileoE1E5a.satelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelGalileoE1E5a.satelliteMetadata.value?.numSignalsTotal)
|
||||
assertTrue(modelGalileoE1E5a.supportedGnssCfs.contains("E1"))
|
||||
assertTrue(modelGalileoE1E5a.supportedGnssCfs.contains("E5a"))
|
||||
} else {
|
||||
assertFalse(modelGalileoE1E5a.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGalileoE1E5a.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGalileoE1E5a.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGalileoE1E5a.isDualFrequencyPerSatInUse)
|
||||
// Because carrier frequency isn't considered, these signals should be detected as duplicates
|
||||
assertEquals(1, modelGalileoE1E5a.duplicateCarrierStatuses.size)
|
||||
assertEquals(0, modelGalileoE1E5a.supportedGnssCfs.size)
|
||||
}
|
||||
|
||||
modelGalileoE1E5a.reset()
|
||||
|
||||
// Test WAAS SBAS - L1 - should be 1 satellite, dual frequency not in use, no non-primary carrier of dual-freq
|
||||
val modelWaasL1L5 = DeviceInfoViewModel(InstrumentationRegistry.getTargetContext().applicationContext as Application)
|
||||
modelWaasL1L5.setStatuses(null, listOf(galaxy15_135L1(true)))
|
||||
assertEquals(1, modelWaasL1L5.sbasSatellites.value?.size)
|
||||
assertFalse(modelWaasL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelWaasL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelWaasL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelWaasL1L5.isDualFrequencyPerSatInUse)
|
||||
assertEquals(1, modelWaasL1L5.satelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(1, modelWaasL1L5.satelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelWaasL1L5.satelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(1, modelWaasL1L5.satelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(1, modelWaasL1L5.satelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(1, modelWaasL1L5.satelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(0, modelWaasL1L5.supportedGnss.size)
|
||||
assertEquals(0, modelWaasL1L5.supportedGnssCfs.size)
|
||||
assertEquals(1, modelWaasL1L5.supportedSbas.size)
|
||||
assertTrue(modelWaasL1L5.supportedSbas.contains(SbasType.WAAS))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertEquals(1, modelWaasL1L5.supportedSbasCfs.size)
|
||||
assertTrue(modelWaasL1L5.supportedSbasCfs.contains("L1"))
|
||||
} else {
|
||||
assertEquals(0, modelWaasL1L5.supportedSbasCfs.size)
|
||||
}
|
||||
|
||||
modelWaasL1L5.reset()
|
||||
|
||||
// Test WAAS SBAS - L1 + L5 - should be 1 satellites, dual frequency in use, non-primary carrier of dual-freq
|
||||
modelWaasL1L5.setStatuses(null, listOf(galaxy15_135L1(true), galaxy15_135L5(true)))
|
||||
assertEquals(1, modelWaasL1L5.sbasSatellites.value?.size)
|
||||
assertEquals(0, modelWaasL1L5.supportedGnss.size)
|
||||
assertEquals(0, modelWaasL1L5.supportedGnssCfs.size)
|
||||
assertEquals(1, modelWaasL1L5.supportedSbas.size)
|
||||
assertTrue(modelWaasL1L5.supportedSbas.contains(SbasType.WAAS))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertTrue(modelWaasL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertTrue(modelWaasL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertTrue(modelWaasL1L5.isDualFrequencyPerSatInView)
|
||||
assertTrue(modelWaasL1L5.isDualFrequencyPerSatInUse)
|
||||
assertEquals(1, modelWaasL1L5.satelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(1, modelWaasL1L5.satelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelWaasL1L5.satelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(2, modelWaasL1L5.satelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(2, modelWaasL1L5.satelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelWaasL1L5.satelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(2, modelWaasL1L5.supportedSbasCfs.size)
|
||||
assertTrue(modelWaasL1L5.supportedSbasCfs.contains("L1"))
|
||||
assertTrue(modelWaasL1L5.supportedSbasCfs.contains("L5"))
|
||||
} else {
|
||||
assertFalse(modelWaasL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelWaasL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelWaasL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelWaasL1L5.isDualFrequencyPerSatInUse)
|
||||
// Because carrier frequency isn't considered, these signals should be detected as duplicates
|
||||
assertEquals(1, modelWaasL1L5.duplicateCarrierStatuses.size)
|
||||
assertEquals(0, modelWaasL1L5.supportedSbasCfs.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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
|
||||
|
||||
import android.location.GnssAntennaInfo
|
||||
import android.location.Location
|
||||
import android.os.Build
|
||||
import androidx.test.filters.SdkSuppress
|
||||
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
|
||||
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
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4ClassRunner::class)
|
||||
class FormatUtilsTest {
|
||||
|
||||
@Test
|
||||
fun locationToLog() {
|
||||
val l = Location("test")
|
||||
l.latitude = 45.34567899
|
||||
l.longitude = 12.45678901
|
||||
l.altitude = 56.2
|
||||
l.speed = 19.2f
|
||||
l.accuracy = 98.7f
|
||||
l.bearing = 100.1f
|
||||
l.time = 12345
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
l.speedAccuracyMetersPerSecond = 382.7f
|
||||
l.bearingAccuracyDegrees = 284.1f
|
||||
l.verticalAccuracyMeters = 583.4f
|
||||
}
|
||||
l.elapsedRealtimeNanos = 123456789
|
||||
|
||||
// Fix,Provider,LatitudeDegrees,LongitudeDegrees,AltitudeMeters,SpeedMps,AccuracyMeters,BearingDegrees,UnixTimeMillis,SpeedAccuracyMps,BearingAccuracyDegrees,elapsedRealtimeNanos,VerticalAccuracyMeters,MockLocation
|
||||
if (l.isSpeedAccuracySupported() && l.isBearingAccuracySupported() && l.isVerticalAccuracySupported()) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
assertEquals(
|
||||
"Fix,test,45.34567899,12.45678901,56.2,19.2,98.7,100.1,12345,382.7,284.1,123456789,583.4,0",
|
||||
l.toLog()
|
||||
)
|
||||
} else {
|
||||
assertEquals(
|
||||
"Fix,test,45.34567899,12.45678901,56.2,19.2,98.7,100.1,12345,382.7,284.1,123456789,583.4,",
|
||||
l.toLog()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
assertEquals(
|
||||
"Fix,test,45.34567899,12.45678901,56.2,19.2,98.7,100.1,12345,,,123456789,,",
|
||||
l.toLog()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun locationToLog_NoExponentialNotation() {
|
||||
val l = Location("test")
|
||||
l.latitude = 0.00000000000000000001
|
||||
l.longitude = 0.00000000000000000001
|
||||
l.altitude = 0.00000000000000000001
|
||||
l.speed = 0.00000000000000000001f
|
||||
l.accuracy = 0.00000000000000000001f
|
||||
l.bearing = 0.00000000000000000001f
|
||||
l.time = 12345
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
l.speedAccuracyMetersPerSecond = 0.00000000000000000001f
|
||||
l.bearingAccuracyDegrees = 0.00000000000000000001f
|
||||
l.verticalAccuracyMeters = 0.00000000000000000001f
|
||||
}
|
||||
l.elapsedRealtimeNanos = 123456789
|
||||
|
||||
// Fix,Provider,LatitudeDegrees,LongitudeDegrees,AltitudeMeters,SpeedMps,AccuracyMeters,BearingDegrees,UnixTimeMillis,SpeedAccuracyMps,BearingAccuracyDegrees,elapsedRealtimeNanos,VerticalAccuracyMeters
|
||||
if (l.isSpeedAccuracySupported() && l.isBearingAccuracySupported() && l.isVerticalAccuracySupported()) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
assertEquals(
|
||||
"Fix,test,0.000000000000000000010,0.000000000000000000010,0.000000000000000000010,0.000000000000000000010,0.000000000000000000010,0.000000000000000000010,12345,0.000000000000000000010,0.000000000000000000010,123456789,0.000000000000000000010,0",
|
||||
l.toLog()
|
||||
)
|
||||
} else {
|
||||
assertEquals(
|
||||
"Fix,test,0.000000000000000000010,0.000000000000000000010,0.000000000000000000010,0.000000000000000000010,0.000000000000000000010,0.000000000000000000010,12345,0.000000000000000000010,0.000000000000000000010,123456789,0.000000000000000000010,",
|
||||
l.toLog()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
assertEquals(
|
||||
"Fix,test,0.000000000000000000010,0.000000000000000000010,0.000000000000000000010,0.000000000000000000010,0.000000000000000000010,0.000000000000000000010,12345,,,123456789,,",
|
||||
l.toLog()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun locationToLog_MockLocation() {
|
||||
val l = Location("test")
|
||||
l.latitude = 45.34567899
|
||||
l.longitude = 12.45678901
|
||||
l.time = 12345
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
l.isMock = true
|
||||
}
|
||||
|
||||
// Fix,Provider,LatitudeDegrees,LongitudeDegrees,AltitudeMeters,SpeedMps,AccuracyMeters,BearingDegrees,UnixTimeMillis,SpeedAccuracyMps,BearingAccuracyDegrees,elapsedRealtimeNanos,VerticalAccuracyMeters,MockLocation
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
assertEquals(
|
||||
"Fix,test,45.34567899,12.45678901,0.0,0.0,0.0,0.0,12345,,,0,,1",
|
||||
l.toLog()
|
||||
)
|
||||
} else {
|
||||
assertEquals(
|
||||
"Fix,test,45.34567899,12.45678901,0.0,0.0,0.0,0.0,12345,,,0,,",
|
||||
l.toLog()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test writing GnssAntennaInfo to CSV format (only runs on Android R or higher)
|
||||
*/
|
||||
@Test
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
|
||||
fun testSerializeGnssAntennaInfo() {
|
||||
val builder = GnssAntennaInfo.Builder()
|
||||
builder.setCarrierFrequencyMHz(1575.42)
|
||||
builder.setPhaseCenterOffset(
|
||||
GnssAntennaInfo.PhaseCenterOffset(
|
||||
1.2,0.1,
|
||||
3.4,0.2,
|
||||
5.6,0.3))
|
||||
builder.setPhaseCenterVariationCorrections(
|
||||
GnssAntennaInfo.SphericalCorrections(
|
||||
buildPhaseCenterVariationCorrectionsArray(),
|
||||
buildPhaseCenterVariationCorrectionsUncertaintyArray()
|
||||
)
|
||||
)
|
||||
builder.setSignalGainCorrections(
|
||||
GnssAntennaInfo.SphericalCorrections(
|
||||
buildSignalGainCorrectionsArray(),
|
||||
buildSignalGainCorrectionsUncertaintyArray()
|
||||
)
|
||||
)
|
||||
|
||||
val expected = "GnssAntennaInfo,1575.42,1.2,0.1,3.4,0.2,5.6,0.3," +
|
||||
"[11.22 33.44 55.66 77.88; 10.2 30.4 50.6 70.8; 12.2 34.4 56.6 78.8]," +
|
||||
"[0.1 0.2 0.3 0.4; 1.1 1.2 1.3 1.4; 2.1 2.2 2.3 2.4],60.0,120.0," +
|
||||
"[9.8 8.7 7.6 6.5; 5.4 4.3 3.2 2.1; 1.3 2.4 3.5 4.6]," +
|
||||
"[0.11 0.22 0.33 0.44; 0.55 0.66 0.77 0.88; 0.91 0.92 0.93 0.94],60.0,120.0"
|
||||
Assert.assertEquals(expected, builder.build().toLog())
|
||||
}
|
||||
|
||||
/**
|
||||
* Test writing array of doubles to String (for serializing GnssAntennaInfo)
|
||||
*/
|
||||
@Test
|
||||
fun testSerializeDoubleArray() {
|
||||
val data = buildPhaseCenterVariationCorrectionsArray()
|
||||
|
||||
val expected = "[11.22 33.44 55.66 77.88; 10.2 30.4 50.6 70.8; 12.2 34.4 56.6 78.8]"
|
||||
Assert.assertEquals(expected, IOUtils.serialize(data))
|
||||
}
|
||||
|
||||
private fun buildPhaseCenterVariationCorrectionsArray() : Array<DoubleArray> {
|
||||
val array1: DoubleArray = doubleArrayOf(11.22, 33.44, 55.66, 77.88)
|
||||
val array2: DoubleArray = doubleArrayOf(10.2, 30.4, 50.6, 70.8)
|
||||
val array3: DoubleArray = doubleArrayOf(12.2, 34.4, 56.6, 78.8)
|
||||
return arrayOf(array1, array2, array3)
|
||||
}
|
||||
|
||||
private fun buildPhaseCenterVariationCorrectionsUncertaintyArray() : Array<DoubleArray> {
|
||||
val array1: DoubleArray = doubleArrayOf(0.1, 0.2, 0.3, 0.4)
|
||||
val array2: DoubleArray = doubleArrayOf(1.1, 1.2, 1.3, 1.4)
|
||||
val array3: DoubleArray = doubleArrayOf(2.1, 2.2, 2.3, 2.4)
|
||||
return arrayOf(array1, array2, array3)
|
||||
}
|
||||
|
||||
private fun buildSignalGainCorrectionsArray() : Array<DoubleArray> {
|
||||
val array1: DoubleArray = doubleArrayOf(9.8, 8.7, 7.6, 6.5)
|
||||
val array2: DoubleArray = doubleArrayOf(5.4, 4.3, 3.2, 2.1)
|
||||
val array3: DoubleArray = doubleArrayOf(1.3, 2.4, 3.5, 4.6)
|
||||
return arrayOf(array1, array2, array3)
|
||||
}
|
||||
|
||||
private fun buildSignalGainCorrectionsUncertaintyArray() : Array<DoubleArray> {
|
||||
val array1: DoubleArray = doubleArrayOf(0.11, 0.22, 0.33, 0.44)
|
||||
val array2: DoubleArray = doubleArrayOf(0.55, 0.66, 0.77, 0.88)
|
||||
val array3: DoubleArray = doubleArrayOf(0.91, 0.92, 0.93, 0.94)
|
||||
return arrayOf(array1, array2, array3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSatelliteToLog() {
|
||||
val location = Location("test")
|
||||
location.time = 0
|
||||
|
||||
val signalCount = 25
|
||||
val signalIndex = 0
|
||||
|
||||
val gpsL1 = SatelliteStatus(
|
||||
10,
|
||||
GnssType.NAVSTAR,
|
||||
35.00f,
|
||||
hasAlmanac = true,
|
||||
hasEphemeris = true,
|
||||
usedInFix = true,
|
||||
elevationDegrees = 57.00f,
|
||||
azimuthDegrees = 136.00f
|
||||
);
|
||||
gpsL1.hasCarrierFrequency = true
|
||||
gpsL1.carrierFrequencyHz = 1575420032.0
|
||||
gpsL1.hasBasebandCn0DbHz = true
|
||||
gpsL1.basebandCn0DbHz = 30.0f
|
||||
assertEquals(
|
||||
"Status,0,25,0,1,10,1575420032,35.0,136.0,57.0,1,1,1,30.0",
|
||||
gpsL1.toLog(location.time, signalCount, signalIndex)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOrientationToLog() {
|
||||
val currentTime = 1234L
|
||||
val timeAtBoot = 1000L
|
||||
|
||||
assertEquals(
|
||||
"OrientationDeg,244,10000000,44444.44444,5555.5555,6666.66666",
|
||||
Orientation(10000000, doubleArrayOf(44444.44444, 5555.5555, 6666.66666)).toLog(
|
||||
currentTime,
|
||||
timeAtBoot
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -16,13 +16,14 @@
|
||||
package com.android.gpstest
|
||||
|
||||
import android.content.Intent
|
||||
import android.location.GnssAntennaInfo
|
||||
import android.location.Location
|
||||
import android.os.Build
|
||||
import androidx.test.filters.SdkSuppress
|
||||
import androidx.test.InstrumentationRegistry.getTargetContext
|
||||
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
|
||||
import com.android.gpstest.util.IOUtils
|
||||
import junit.framework.Assert.*
|
||||
import com.android.gpstest.library.util.IOUtils
|
||||
import junit.framework.Assert.assertEquals
|
||||
import junit.framework.Assert.assertFalse
|
||||
import junit.framework.Assert.assertNull
|
||||
import junit.framework.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@@ -40,11 +41,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));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,7 +62,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())
|
||||
@@ -72,7 +73,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)
|
||||
@@ -82,7 +83,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())
|
||||
@@ -93,7 +94,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)
|
||||
@@ -104,7 +105,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())
|
||||
@@ -115,7 +116,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)
|
||||
@@ -126,12 +127,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"))
|
||||
@@ -140,7 +141,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"))
|
||||
@@ -150,7 +151,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"))
|
||||
@@ -163,61 +164,64 @@ 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)
|
||||
|
||||
assertNull(IOUtils.getLocationFromGeoUri(getTargetContext(), "geo:,43.06480"))
|
||||
assertNull(IOUtils.getLocationFromGeoUri(getTargetContext(), "geo:37.786971,"))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,17 +232,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)
|
||||
}
|
||||
|
||||
@@ -294,73 +298,4 @@ class IOUtilsTest {
|
||||
val input2 = "[GPS, GLONASS]"
|
||||
assertEquals("[GPS, GLONASS]", IOUtils.replaceNavstar(input2))
|
||||
}
|
||||
|
||||
/**
|
||||
* Test writing array of doubles to String (for serializing GnssAntennaInfo)
|
||||
*/
|
||||
@Test
|
||||
fun testSerializeDoubleArray() {
|
||||
val data = buildPhaseCenterVariationCorrectionsArray()
|
||||
|
||||
val expected = "[11.22 33.44 55.66 77.88; 10.2 30.4 50.6 70.8; 12.2 34.4 56.6 78.8]"
|
||||
assertEquals(expected, IOUtils.serialize(data))
|
||||
}
|
||||
|
||||
/**
|
||||
* Test writing GnssAntennaInfo to CSV format (only runs on Android R or higher)
|
||||
*/
|
||||
@Test
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
|
||||
fun testSerializeGnssAntennaInfo() {
|
||||
val builder = GnssAntennaInfo.Builder()
|
||||
builder.setCarrierFrequencyMHz(1575.42)
|
||||
builder.setPhaseCenterOffset(
|
||||
GnssAntennaInfo.PhaseCenterOffset(
|
||||
1.2,0.1,
|
||||
3.4,0.2,
|
||||
5.6,0.3))
|
||||
builder.setPhaseCenterVariationCorrections(
|
||||
GnssAntennaInfo.SphericalCorrections(
|
||||
buildPhaseCenterVariationCorrectionsArray(),
|
||||
buildPhaseCenterVariationCorrectionsUncertaintyArray()))
|
||||
builder.setSignalGainCorrections(
|
||||
GnssAntennaInfo.SphericalCorrections(
|
||||
buildSignalGainCorrectionsArray(),
|
||||
buildSignalGainCorrectionsUnvesrtaintyArray()))
|
||||
|
||||
val expected = "GnssAntennaInfo,1575.42,1.2,0.1,3.4,0.2,5.6,0.3," +
|
||||
"[11.22 33.44 55.66 77.88; 10.2 30.4 50.6 70.8; 12.2 34.4 56.6 78.8]," +
|
||||
"[0.1 0.2 0.3 0.4; 1.1 1.2 1.3 1.4; 2.1 2.2 2.3 2.4],60.0,120.0," +
|
||||
"[9.8 8.7 7.6 6.5; 5.4 4.3 3.2 2.1; 1.3 2.4 3.5 4.6]," +
|
||||
"[0.11 0.22 0.33 0.44; 0.55 0.66 0.77 0.88; 0.91 0.92 0.93 0.94],60.0,120.0"
|
||||
assertEquals(expected, IOUtils.serialize(builder.build()))
|
||||
}
|
||||
|
||||
fun buildPhaseCenterVariationCorrectionsArray() : Array<DoubleArray> {
|
||||
val array1: DoubleArray = doubleArrayOf(11.22, 33.44, 55.66, 77.88)
|
||||
val array2: DoubleArray = doubleArrayOf(10.2, 30.4, 50.6, 70.8)
|
||||
val array3: DoubleArray = doubleArrayOf(12.2, 34.4, 56.6, 78.8)
|
||||
return arrayOf(array1, array2, array3)
|
||||
}
|
||||
|
||||
fun buildPhaseCenterVariationCorrectionsUncertaintyArray() : Array<DoubleArray> {
|
||||
val array1: DoubleArray = doubleArrayOf(0.1, 0.2, 0.3, 0.4)
|
||||
val array2: DoubleArray = doubleArrayOf(1.1, 1.2, 1.3, 1.4)
|
||||
val array3: DoubleArray = doubleArrayOf(2.1, 2.2, 2.3, 2.4)
|
||||
return arrayOf(array1, array2, array3)
|
||||
}
|
||||
|
||||
fun buildSignalGainCorrectionsArray() : Array<DoubleArray> {
|
||||
val array1: DoubleArray = doubleArrayOf(9.8, 8.7, 7.6, 6.5)
|
||||
val array2: DoubleArray = doubleArrayOf(5.4, 4.3, 3.2, 2.1)
|
||||
val array3: DoubleArray = doubleArrayOf(1.3, 2.4, 3.5, 4.6)
|
||||
return arrayOf(array1, array2, array3)
|
||||
}
|
||||
|
||||
fun buildSignalGainCorrectionsUnvesrtaintyArray() : Array<DoubleArray> {
|
||||
val array1: DoubleArray = doubleArrayOf(0.11, 0.22, 0.33, 0.44)
|
||||
val array2: DoubleArray = doubleArrayOf(0.55, 0.66, 0.77, 0.88)
|
||||
val array3: DoubleArray = doubleArrayOf(0.91, 0.92, 0.93, 0.94)
|
||||
return arrayOf(array1, array2, array3)
|
||||
}
|
||||
}
|
||||
@@ -15,22 +15,22 @@
|
||||
*/
|
||||
package com.android.gpstest;
|
||||
|
||||
import static androidx.test.InstrumentationRegistry.getTargetContext;
|
||||
import static junit.framework.Assert.assertFalse;
|
||||
import static junit.framework.Assert.assertTrue;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
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;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import static androidx.test.InstrumentationRegistry.getTargetContext;
|
||||
import static junit.framework.Assert.assertFalse;
|
||||
import static junit.framework.Assert.assertTrue;
|
||||
|
||||
@RunWith(AndroidJUnit4ClassRunner.class)
|
||||
public class LocationUtilsTest {
|
||||
|
||||
|
||||
@@ -15,17 +15,17 @@
|
||||
*/
|
||||
package com.android.gpstest;
|
||||
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
|
||||
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;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
|
||||
@RunWith(AndroidJUnit4ClassRunner.class)
|
||||
public class MathUtilsAndroidTest {
|
||||
|
||||
|
||||
@@ -15,14 +15,21 @@
|
||||
*/
|
||||
package com.android.gpstest
|
||||
|
||||
import android.location.GnssMeasurement.*
|
||||
import android.location.GnssMeasurement.ADR_STATE_CYCLE_SLIP
|
||||
import android.location.GnssMeasurement.ADR_STATE_HALF_CYCLE_REPORTED
|
||||
import android.location.GnssMeasurement.ADR_STATE_HALF_CYCLE_RESOLVED
|
||||
import android.location.GnssMeasurement.ADR_STATE_RESET
|
||||
import android.location.GnssMeasurement.ADR_STATE_UNKNOWN
|
||||
import android.location.GnssMeasurement.ADR_STATE_VALID
|
||||
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 org.junit.Assert.*
|
||||
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.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@@ -47,7 +54,7 @@ class SatelliteUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
gpsL1.hasCarrierFrequency = true
|
||||
gpsL1.carrierFrequencyHz = 1575420000.0f
|
||||
gpsL1.carrierFrequencyHz = 1575420000.0
|
||||
|
||||
val gpsL1key = SatelliteUtils.createGnssSatelliteKey(gpsL1)
|
||||
assertEquals("1 NAVSTAR", gpsL1key)
|
||||
@@ -75,7 +82,7 @@ class SatelliteUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
sbasWaasL1.hasCarrierFrequency = true
|
||||
sbasWaasL1.carrierFrequencyHz = 1575420000.0f
|
||||
sbasWaasL1.carrierFrequencyHz = 1575420000.0
|
||||
sbasWaasL1.sbasType = SbasType.WAAS
|
||||
|
||||
val sbasWaasL1key = SatelliteUtils.createGnssSatelliteKey(sbasWaasL1)
|
||||
@@ -119,7 +126,7 @@ class SatelliteUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
sbasSdcm125L1WithCf.hasCarrierFrequency = true
|
||||
sbasSdcm125L1WithCf.carrierFrequencyHz = 1575420000.0f
|
||||
sbasSdcm125L1WithCf.carrierFrequencyHz = 1575420000.0
|
||||
sbasSdcm125L1WithCf.sbasType = SbasType.SDCM
|
||||
|
||||
val sbasSdcm125L1WithCfkey = SatelliteUtils.createGnssSatelliteKey(sbasSdcm125L1WithCf)
|
||||
@@ -141,7 +148,7 @@ class SatelliteUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
gpsL1.hasCarrierFrequency = true
|
||||
gpsL1.carrierFrequencyHz = 1575420000.0f
|
||||
gpsL1.carrierFrequencyHz = 1575420000.0
|
||||
|
||||
val gpsL1key = SatelliteUtils.createGnssStatusKey(gpsL1)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@@ -173,7 +180,7 @@ class SatelliteUtilsTest {
|
||||
72f,
|
||||
25f);
|
||||
gpsBadCf.hasCarrierFrequency = true
|
||||
gpsBadCf.carrierFrequencyHz = 9999999.0f
|
||||
gpsBadCf.carrierFrequencyHz = 9999999.0
|
||||
|
||||
val gpsBadCfKey = SatelliteUtils.createGnssStatusKey(gpsBadCf)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
|
||||
@@ -0,0 +1,482 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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.
|
||||
*/
|
||||
|
||||
package com.android.gpstest
|
||||
|
||||
import android.app.Application
|
||||
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.library.data.LocationRepository
|
||||
import com.android.gpstest.library.data.SharedAntennaManager
|
||||
import com.android.gpstest.library.data.SharedGnssMeasurementManager
|
||||
import com.android.gpstest.library.data.SharedGnssStatusManager
|
||||
import com.android.gpstest.library.data.SharedLocationManager
|
||||
import com.android.gpstest.library.data.SharedNavMessageManager
|
||||
import com.android.gpstest.library.data.SharedNmeaManager
|
||||
import com.android.gpstest.library.data.SharedSensorManager
|
||||
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.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4ClassRunner::class)
|
||||
class SignalInfoViewModelTest {
|
||||
|
||||
// Required to allow LiveData to execute
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
private val repository = LocationRepository(
|
||||
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()))
|
||||
)
|
||||
|
||||
/**
|
||||
* Test aggregating signal information into satellites
|
||||
*/
|
||||
@Test
|
||||
fun testDeviceInfoViewModel() {
|
||||
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(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)
|
||||
assertFalse(modelGpsL1.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1.isDualFrequencyPerSatInUse)
|
||||
assertEquals(1, modelGpsL1.filteredSatelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(1, modelGpsL1.filteredSatelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelGpsL1.filteredSatelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(1, modelGpsL1.filteredSatelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(1, modelGpsL1.filteredSatelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(1, modelGpsL1.filteredSatelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(1, modelGpsL1.getSupportedGnss().size)
|
||||
assertEquals(0, modelGpsL1.getSupportedSbas().size)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertEquals(1, modelGpsL1.getSupportedGnssCfs().size)
|
||||
assertTrue(modelGpsL1.getSupportedGnssCfs().contains("L1"))
|
||||
} else {
|
||||
assertEquals(0, modelGpsL1.getSupportedGnssCfs().size)
|
||||
}
|
||||
assertEquals(0, modelGpsL1.getSupportedSbasCfs().size)
|
||||
assertTrue(modelGpsL1.getSupportedGnss().contains(GnssType.NAVSTAR))
|
||||
|
||||
modelGpsL1.reset();
|
||||
|
||||
// Test GPS L1 no signal - should be 1 satellite, no L5 or dual-frequency
|
||||
modelGpsL1.updateStatus(context, listOf(gpsL1NoSignal(1)), PreferenceManager.getDefaultSharedPreferences(context))
|
||||
assertEquals(1, modelGpsL1.filteredGnssSatellites.value?.size)
|
||||
assertFalse(modelGpsL1.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGpsL1.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL1.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1.isDualFrequencyPerSatInUse)
|
||||
assertEquals(0, modelGpsL1.filteredSatelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(0, modelGpsL1.filteredSatelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelGpsL1.filteredSatelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(0, modelGpsL1.filteredSatelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(0, modelGpsL1.filteredSatelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(1, modelGpsL1.filteredSatelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(1, modelGpsL1.getSupportedGnss().size)
|
||||
assertEquals(0, modelGpsL1.getSupportedSbas().size)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertEquals(1, modelGpsL1.getSupportedGnssCfs().size)
|
||||
assertTrue(modelGpsL1.getSupportedGnssCfs().contains("L1"))
|
||||
} else {
|
||||
assertEquals(0, modelGpsL1.getSupportedGnssCfs().size)
|
||||
}
|
||||
assertEquals(0, modelGpsL1.getSupportedSbasCfs().size)
|
||||
assertTrue(modelGpsL1.getSupportedGnss().contains(GnssType.NAVSTAR))
|
||||
|
||||
|
||||
// Test GPS L1 + L5 same sv - should be 1 satellite, dual frequency in view and but not in use
|
||||
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)
|
||||
assertEquals(0, modelGpsL1L5.getSupportedSbasCfs().size)
|
||||
assertTrue(modelGpsL1L5.getSupportedGnss().contains(GnssType.NAVSTAR))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertTrue(modelGpsL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertTrue(modelGpsL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertTrue(modelGpsL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInUse)
|
||||
assertEquals(1, modelGpsL1L5.filteredSatelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(1, modelGpsL1L5.filteredSatelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelGpsL1L5.filteredSatelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(2, modelGpsL1L5.filteredSatelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(1, modelGpsL1L5.filteredSatelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelGpsL1L5.filteredSatelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(2, modelGpsL1L5.getSupportedGnssCfs().size)
|
||||
assertTrue(modelGpsL1L5.getSupportedGnssCfs().contains("L1"))
|
||||
assertTrue(modelGpsL1L5.getSupportedGnssCfs().contains("L5"))
|
||||
} else {
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInUse)
|
||||
// Because carrier frequency isn't considered, these signals should be detected as duplicates
|
||||
assertEquals(1, modelGpsL1L5.duplicateCarrierStatuses.size)
|
||||
assertEquals(0, modelGpsL1L5.getSupportedGnssCfs().size)
|
||||
}
|
||||
|
||||
modelGpsL1L5.reset();
|
||||
|
||||
// Test GPS L1 + L5 same sv - should be 1 satellite, dual-frequency in view and use
|
||||
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)
|
||||
assertEquals(0, modelGpsL1L5.getSupportedSbasCfs().size)
|
||||
assertTrue(modelGpsL1L5.getSupportedGnss().contains(GnssType.NAVSTAR))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertTrue(modelGpsL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertTrue(modelGpsL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertTrue(modelGpsL1L5.isDualFrequencyPerSatInView)
|
||||
assertTrue(modelGpsL1L5.isDualFrequencyPerSatInUse)
|
||||
assertEquals(1, modelGpsL1L5.filteredSatelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(1, modelGpsL1L5.filteredSatelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelGpsL1L5.filteredSatelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(2, modelGpsL1L5.filteredSatelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(2, modelGpsL1L5.filteredSatelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelGpsL1L5.filteredSatelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(2, modelGpsL1L5.getSupportedGnssCfs().size)
|
||||
assertTrue(modelGpsL1L5.getSupportedGnssCfs().contains("L1"))
|
||||
assertTrue(modelGpsL1L5.getSupportedGnssCfs().contains("L5"))
|
||||
} else {
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInUse)
|
||||
// Because carrier frequency isn't considered, these signals should be detected as duplicates
|
||||
assertEquals(1, modelGpsL1L5.duplicateCarrierStatuses.size)
|
||||
assertEquals(0, modelGpsL1L5.getSupportedGnssCfs().size)
|
||||
}
|
||||
|
||||
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(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)
|
||||
assertEquals(0, modelGpsL1L5.getSupportedSbasCfs().size)
|
||||
assertTrue(modelGpsL1L5.getSupportedGnss().contains(GnssType.NAVSTAR))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertTrue(modelGpsL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertTrue(modelGpsL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInUse)
|
||||
assertEquals(1, modelGpsL1L5.filteredSatelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(1, modelGpsL1L5.filteredSatelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelGpsL1L5.filteredSatelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(2, modelGpsL1L5.filteredSatelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(1, modelGpsL1L5.filteredSatelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelGpsL1L5.filteredSatelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(2, modelGpsL1L5.getSupportedGnssCfs().size)
|
||||
assertTrue(modelGpsL1L5.getSupportedGnssCfs().contains("L1"))
|
||||
assertTrue(modelGpsL1L5.getSupportedGnssCfs().contains("L5"))
|
||||
} else {
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInUse)
|
||||
// Because carrier frequency isn't considered, these signals should be detected as duplicates
|
||||
assertEquals(1, modelGpsL1L5.duplicateCarrierStatuses.size)
|
||||
assertEquals(0, modelGpsL1L5.getSupportedGnssCfs().size)
|
||||
}
|
||||
|
||||
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(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)
|
||||
assertEquals(0, modelGpsL1L5.getSupportedSbasCfs().size)
|
||||
assertTrue(modelGpsL1L5.getSupportedGnss().contains(GnssType.NAVSTAR))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertTrue(modelGpsL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertTrue(modelGpsL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInUse)
|
||||
assertEquals(2, modelGpsL1L5.filteredSatelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(2, modelGpsL1L5.filteredSatelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(2, modelGpsL1L5.filteredSatelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(2, modelGpsL1L5.filteredSatelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(2, modelGpsL1L5.filteredSatelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelGpsL1L5.filteredSatelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(2, modelGpsL1L5.getSupportedGnssCfs().size)
|
||||
assertTrue(modelGpsL1L5.getSupportedGnssCfs().contains("L1"))
|
||||
assertTrue(modelGpsL1L5.getSupportedGnssCfs().contains("L5"))
|
||||
} else {
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInUse)
|
||||
assertEquals(2, modelGpsL1L5.filteredSatelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(2, modelGpsL1L5.filteredSatelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(2, modelGpsL1L5.filteredSatelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(2, modelGpsL1L5.filteredSatelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(2, modelGpsL1L5.filteredSatelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelGpsL1L5.filteredSatelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(0, modelGpsL1L5.getSupportedGnssCfs().size)
|
||||
}
|
||||
|
||||
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(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)
|
||||
assertEquals(0, modelGpsL1L5.getSupportedSbasCfs().size)
|
||||
assertTrue(modelGpsL1L5.getSupportedGnss().contains(GnssType.NAVSTAR))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertTrue(modelGpsL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertTrue(modelGpsL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInUse)
|
||||
assertEquals(1, modelGpsL1L5.filteredSatelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(1, modelGpsL1L5.filteredSatelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelGpsL1L5.filteredSatelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(1, modelGpsL1L5.filteredSatelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(1, modelGpsL1L5.filteredSatelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelGpsL1L5.filteredSatelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(2, modelGpsL1L5.getSupportedGnssCfs().size)
|
||||
assertTrue(modelGpsL1L5.getSupportedGnssCfs().contains("L1"))
|
||||
assertTrue(modelGpsL1L5.getSupportedGnssCfs().contains("L5"))
|
||||
} else {
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGpsL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1L5.isDualFrequencyPerSatInUse)
|
||||
// Because carrier frequency isn't considered, these signals should be detected as duplicates
|
||||
assertEquals(1, modelGpsL1L5.duplicateCarrierStatuses.size)
|
||||
assertEquals(0, modelGpsL1L5.getSupportedGnssCfs().size)
|
||||
}
|
||||
|
||||
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(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)
|
||||
assertEquals(0, modelGpsL5.getSupportedSbasCfs().size)
|
||||
assertTrue(modelGpsL5.getSupportedGnss().contains(GnssType.NAVSTAR))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertTrue(modelGpsL5.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGpsL5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL5.isDualFrequencyPerSatInUse)
|
||||
assertEquals(1, modelGpsL5.filteredSatelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(0, modelGpsL5.filteredSatelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelGpsL5.filteredSatelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(1, modelGpsL5.filteredSatelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(0, modelGpsL5.filteredSatelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(1, modelGpsL5.filteredSatelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(1, modelGpsL5.getSupportedGnssCfs().size)
|
||||
assertTrue(modelGpsL5.getSupportedGnssCfs().contains("L5"))
|
||||
} else {
|
||||
assertFalse(modelGpsL5.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGpsL5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGpsL5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL5.isDualFrequencyPerSatInUse)
|
||||
assertEquals(1, modelGpsL5.filteredSatelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(0, modelGpsL5.filteredSatelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelGpsL5.filteredSatelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(1, modelGpsL5.filteredSatelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(0, modelGpsL5.filteredSatelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(1, modelGpsL5.filteredSatelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(0, modelGpsL5.getSupportedGnssCfs().size)
|
||||
}
|
||||
|
||||
// Test GPS L1 + GLONASS L1 - should be 2 satellites, no non-primary carrier of dual-freq
|
||||
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)
|
||||
assertFalse(modelGpsL1GlonassL1.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGpsL1GlonassL1.isDualFrequencyPerSatInUse)
|
||||
assertEquals(2, modelGpsL1GlonassL1.filteredSatelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(2, modelGpsL1GlonassL1.filteredSatelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(2, modelGpsL1GlonassL1.filteredSatelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(2, modelGpsL1GlonassL1.filteredSatelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(2, modelGpsL1GlonassL1.filteredSatelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelGpsL1GlonassL1.filteredSatelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(2, modelGpsL1GlonassL1.getSupportedGnss().size)
|
||||
assertEquals(0, modelGpsL1GlonassL1.getSupportedSbas().size)
|
||||
assertEquals(0, modelGpsL1GlonassL1.getSupportedSbasCfs().size)
|
||||
assertTrue(modelGpsL1GlonassL1.getSupportedGnss().contains(GnssType.NAVSTAR))
|
||||
assertTrue(modelGpsL1GlonassL1.getSupportedGnss().contains(GnssType.GLONASS))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertEquals(1, modelGpsL1GlonassL1.getSupportedGnssCfs().size)
|
||||
assertTrue(modelGpsL1GlonassL1.getSupportedGnssCfs().contains("L1"))
|
||||
} else {
|
||||
assertEquals(0, modelGpsL1GlonassL1.getSupportedGnssCfs().size)
|
||||
}
|
||||
|
||||
// Test Galileo E1 + E5a - should be 2 satellites, dual frequency not in use, non-primary carrier of dual-freq
|
||||
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)
|
||||
assertEquals(0, modelGalileoE1E5a.getSupportedSbasCfs().size)
|
||||
assertTrue(modelGalileoE1E5a.getSupportedGnss().contains(GnssType.GALILEO))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertTrue(modelGalileoE1E5a.isNonPrimaryCarrierFreqInView)
|
||||
assertTrue(modelGalileoE1E5a.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGalileoE1E5a.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGalileoE1E5a.isDualFrequencyPerSatInUse)
|
||||
assertEquals(2, modelGalileoE1E5a.filteredSatelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(2, modelGalileoE1E5a.filteredSatelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(2, modelGalileoE1E5a.filteredSatelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(2, modelGalileoE1E5a.filteredSatelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(2, modelGalileoE1E5a.filteredSatelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelGalileoE1E5a.filteredSatelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(2, modelGalileoE1E5a.getSupportedGnssCfs().size)
|
||||
assertTrue(modelGalileoE1E5a.getSupportedGnssCfs().contains("E1"))
|
||||
assertTrue(modelGalileoE1E5a.getSupportedGnssCfs().contains("E5a"))
|
||||
} else {
|
||||
assertFalse(modelGalileoE1E5a.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGalileoE1E5a.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGalileoE1E5a.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGalileoE1E5a.isDualFrequencyPerSatInUse)
|
||||
assertEquals(2, modelGalileoE1E5a.filteredSatelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(2, modelGalileoE1E5a.filteredSatelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(2, modelGalileoE1E5a.filteredSatelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(2, modelGalileoE1E5a.filteredSatelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(2, modelGalileoE1E5a.filteredSatelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelGalileoE1E5a.filteredSatelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(0, modelGalileoE1E5a.getSupportedGnssCfs().size)
|
||||
}
|
||||
|
||||
modelGalileoE1E5a.reset()
|
||||
|
||||
// Test Galileo E1 + E5a - should be 1 satellites, dual frequency in use, non-primary carrier of dual-freq
|
||||
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)
|
||||
assertEquals(0, modelGalileoE1E5a.getSupportedSbasCfs().size)
|
||||
assertTrue(modelGalileoE1E5a.getSupportedGnss().contains(GnssType.GALILEO))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertTrue(modelGalileoE1E5a.isNonPrimaryCarrierFreqInView)
|
||||
assertTrue(modelGalileoE1E5a.isNonPrimaryCarrierFreqInUse)
|
||||
assertTrue(modelGalileoE1E5a.isDualFrequencyPerSatInView)
|
||||
assertTrue(modelGalileoE1E5a.isDualFrequencyPerSatInUse)
|
||||
assertEquals(1, modelGalileoE1E5a.filteredSatelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(1, modelGalileoE1E5a.filteredSatelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelGalileoE1E5a.filteredSatelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(2, modelGalileoE1E5a.filteredSatelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(2, modelGalileoE1E5a.filteredSatelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelGalileoE1E5a.filteredSatelliteMetadata.value?.numSignalsTotal)
|
||||
assertTrue(modelGalileoE1E5a.getSupportedGnssCfs().contains("E1"))
|
||||
assertTrue(modelGalileoE1E5a.getSupportedGnssCfs().contains("E5a"))
|
||||
} else {
|
||||
assertFalse(modelGalileoE1E5a.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelGalileoE1E5a.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelGalileoE1E5a.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelGalileoE1E5a.isDualFrequencyPerSatInUse)
|
||||
// Because carrier frequency isn't considered, these signals should be detected as duplicates
|
||||
assertEquals(1, modelGalileoE1E5a.duplicateCarrierStatuses.size)
|
||||
assertEquals(0, modelGalileoE1E5a.getSupportedGnssCfs().size)
|
||||
}
|
||||
|
||||
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(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)
|
||||
assertFalse(modelWaasL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelWaasL1L5.isDualFrequencyPerSatInUse)
|
||||
assertEquals(1, modelWaasL1L5.filteredSatelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(1, modelWaasL1L5.filteredSatelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelWaasL1L5.filteredSatelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(1, modelWaasL1L5.filteredSatelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(1, modelWaasL1L5.filteredSatelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(1, modelWaasL1L5.filteredSatelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(0, modelWaasL1L5.getSupportedGnss().size)
|
||||
assertEquals(0, modelWaasL1L5.getSupportedGnssCfs().size)
|
||||
assertEquals(1, modelWaasL1L5.getSupportedSbas().size)
|
||||
assertTrue(modelWaasL1L5.getSupportedSbas().contains(SbasType.WAAS))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertEquals(1, modelWaasL1L5.getSupportedSbasCfs().size)
|
||||
assertTrue(modelWaasL1L5.getSupportedSbasCfs().contains("L1"))
|
||||
} else {
|
||||
assertEquals(0, modelWaasL1L5.getSupportedSbasCfs().size)
|
||||
}
|
||||
|
||||
modelWaasL1L5.reset()
|
||||
|
||||
// Test WAAS SBAS - L1 + L5 - should be 1 satellites, dual frequency in use, non-primary carrier of dual-freq
|
||||
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)
|
||||
assertEquals(1, modelWaasL1L5.getSupportedSbas().size)
|
||||
assertTrue(modelWaasL1L5.getSupportedSbas().contains(SbasType.WAAS))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
assertTrue(modelWaasL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertTrue(modelWaasL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertTrue(modelWaasL1L5.isDualFrequencyPerSatInView)
|
||||
assertTrue(modelWaasL1L5.isDualFrequencyPerSatInUse)
|
||||
assertEquals(1, modelWaasL1L5.filteredSatelliteMetadata.value?.numSatsInView)
|
||||
assertEquals(1, modelWaasL1L5.filteredSatelliteMetadata.value?.numSatsUsed)
|
||||
assertEquals(1, modelWaasL1L5.filteredSatelliteMetadata.value?.numSatsTotal)
|
||||
assertEquals(2, modelWaasL1L5.filteredSatelliteMetadata.value?.numSignalsInView)
|
||||
assertEquals(2, modelWaasL1L5.filteredSatelliteMetadata.value?.numSignalsUsed)
|
||||
assertEquals(2, modelWaasL1L5.filteredSatelliteMetadata.value?.numSignalsTotal)
|
||||
assertEquals(2, modelWaasL1L5.getSupportedSbasCfs().size)
|
||||
assertTrue(modelWaasL1L5.getSupportedSbasCfs().contains("L1"))
|
||||
assertTrue(modelWaasL1L5.getSupportedSbasCfs().contains("L5"))
|
||||
} else {
|
||||
assertFalse(modelWaasL1L5.isNonPrimaryCarrierFreqInView)
|
||||
assertFalse(modelWaasL1L5.isNonPrimaryCarrierFreqInUse)
|
||||
assertFalse(modelWaasL1L5.isDualFrequencyPerSatInView)
|
||||
assertFalse(modelWaasL1L5.isDualFrequencyPerSatInUse)
|
||||
// Because carrier frequency isn't considered, these signals should be detected as duplicates
|
||||
assertEquals(1, modelWaasL1L5.duplicateCarrierStatuses.size)
|
||||
assertEquals(0, modelWaasL1L5.getSupportedSbasCfs().size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -33,7 +33,7 @@ fun gpsL1(id: Int, usedInFix: Boolean): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
gpsL1.hasCarrierFrequency = true
|
||||
gpsL1.carrierFrequencyHz = 1575420000.0f
|
||||
gpsL1.carrierFrequencyHz = 1575420000.0
|
||||
return gpsL1
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ fun gpsL1NoSignal(id: Int): SatelliteStatus {
|
||||
NO_DATA,
|
||||
NO_DATA);
|
||||
gpsL1.hasCarrierFrequency = true
|
||||
gpsL1.carrierFrequencyHz = 1575420000.0f
|
||||
gpsL1.carrierFrequencyHz = 1575420000.0
|
||||
return gpsL1
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ fun gpsL2(): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
gpsL2.hasCarrierFrequency = true
|
||||
gpsL2.carrierFrequencyHz = 1227600000.0f
|
||||
gpsL2.carrierFrequencyHz = 1227600000.0
|
||||
return gpsL2
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ fun gpsL3(): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
gpsL3.hasCarrierFrequency = true
|
||||
gpsL3.carrierFrequencyHz = 1381050000.0f
|
||||
gpsL3.carrierFrequencyHz = 1381050000.0
|
||||
return gpsL3
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ fun gpsL4(): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
gpsL4.hasCarrierFrequency = true
|
||||
gpsL4.carrierFrequencyHz = 1379913000.0f
|
||||
gpsL4.carrierFrequencyHz = 1379913000.0
|
||||
return gpsL4
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ fun gpsL5(id: Int, usedInFix: Boolean): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
gpsL5.hasCarrierFrequency = true
|
||||
gpsL5.carrierFrequencyHz = 1176450000.0f
|
||||
gpsL5.carrierFrequencyHz = 1176450000.0
|
||||
return gpsL5
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ fun glonassL1variant1(): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
glonassL1variant1.hasCarrierFrequency = true
|
||||
glonassL1variant1.carrierFrequencyHz = 1598062500.0f
|
||||
glonassL1variant1.carrierFrequencyHz = 1598062500.0
|
||||
return glonassL1variant1
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ fun glonassL1variant2(): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
glonassL1variant2.hasCarrierFrequency = true
|
||||
glonassL1variant2.carrierFrequencyHz = 1605375000.0f
|
||||
glonassL1variant2.carrierFrequencyHz = 1605375000.0
|
||||
return glonassL1variant2
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ fun glonassL2variant1(): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
glonassL2.hasCarrierFrequency = true
|
||||
glonassL2.carrierFrequencyHz = 1242937500.0f
|
||||
glonassL2.carrierFrequencyHz = 1242937500.0
|
||||
return glonassL2
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ fun glonassL2variant2(): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
glonassL2variant2.hasCarrierFrequency = true
|
||||
glonassL2variant2.carrierFrequencyHz = 1248625000.0f
|
||||
glonassL2variant2.carrierFrequencyHz = 1248625000.0
|
||||
return glonassL2variant2
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ fun glonassL3(): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
glonassL3.hasCarrierFrequency = true
|
||||
glonassL3.carrierFrequencyHz = 1207140000.0f
|
||||
glonassL3.carrierFrequencyHz = 1207140000.0
|
||||
return glonassL3
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ fun glonassL5(): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
glonassL5.hasCarrierFrequency = true
|
||||
glonassL5.carrierFrequencyHz = 1176450000.0f
|
||||
glonassL5.carrierFrequencyHz = 1176450000.0
|
||||
return glonassL5
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ fun glonassL1Cdma(): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
glonassL1Cdma.hasCarrierFrequency = true
|
||||
glonassL1Cdma.carrierFrequencyHz = 1575420000.0f
|
||||
glonassL1Cdma.carrierFrequencyHz = 1575420000.0
|
||||
return glonassL1Cdma
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ fun galileoE1(id: Int, usedInFix: Boolean): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
galileoE1.hasCarrierFrequency = true
|
||||
galileoE1.carrierFrequencyHz = 1575420000.0f
|
||||
galileoE1.carrierFrequencyHz = 1575420000.0
|
||||
return galileoE1
|
||||
}
|
||||
|
||||
@@ -271,7 +271,7 @@ fun galileoE5(id: Int, usedInFix: Boolean): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
galileoE5.hasCarrierFrequency = true
|
||||
galileoE5.carrierFrequencyHz = 1191795000.0f
|
||||
galileoE5.carrierFrequencyHz = 1191795000.0
|
||||
return galileoE5
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ fun galileoE5a(id: Int, usedInFix: Boolean): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
galileoE5a.hasCarrierFrequency = true
|
||||
galileoE5a.carrierFrequencyHz = 1176450000.0f
|
||||
galileoE5a.carrierFrequencyHz = 1176450000.0
|
||||
return galileoE5a
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ fun galileoE5b(id: Int, usedInFix: Boolean): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
galileoE5b.hasCarrierFrequency = true
|
||||
galileoE5b.carrierFrequencyHz = 1207140000.0f
|
||||
galileoE5b.carrierFrequencyHz = 1207140000.0
|
||||
return galileoE5b
|
||||
}
|
||||
|
||||
@@ -322,7 +322,7 @@ fun galileoE6(id: Int, usedInFix: Boolean): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
galileoE6.hasCarrierFrequency = true
|
||||
galileoE6.carrierFrequencyHz = 1278750000.0f
|
||||
galileoE6.carrierFrequencyHz = 1278750000.0
|
||||
return galileoE6
|
||||
}
|
||||
|
||||
@@ -339,7 +339,7 @@ fun waas_131L1(usedInFix: Boolean): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
waas_131L1.hasCarrierFrequency = true
|
||||
waas_131L1.carrierFrequencyHz = 1575420000.0f
|
||||
waas_131L1.carrierFrequencyHz = 1575420000.0
|
||||
waas_131L1.sbasType = SbasType.WAAS
|
||||
return waas_131L1
|
||||
}
|
||||
@@ -357,7 +357,7 @@ fun waas_131L5(usedInFix: Boolean): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
waas_131L5.hasCarrierFrequency = true
|
||||
waas_131L5.carrierFrequencyHz = 1176450000.0f
|
||||
waas_131L5.carrierFrequencyHz = 1176450000.0
|
||||
waas_131L5.sbasType = SbasType.WAAS
|
||||
return waas_131L5
|
||||
}
|
||||
@@ -375,7 +375,7 @@ fun waas_133L1(usedInFix: Boolean): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
waas_133L1.hasCarrierFrequency = true
|
||||
waas_133L1.carrierFrequencyHz = 1575420000.0f
|
||||
waas_133L1.carrierFrequencyHz = 1575420000.0
|
||||
waas_133L1.sbasType = SbasType.WAAS
|
||||
return waas_133L1
|
||||
}
|
||||
@@ -393,7 +393,7 @@ fun waas_133L5(usedInFix: Boolean): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
waas_133L5.hasCarrierFrequency = true
|
||||
waas_133L5.carrierFrequencyHz = 1176450000.0f
|
||||
waas_133L5.carrierFrequencyHz = 1176450000.0
|
||||
waas_133L5.sbasType = SbasType.WAAS
|
||||
return waas_133L5
|
||||
}
|
||||
@@ -411,7 +411,7 @@ fun galaxy15_135L1(usedInFix: Boolean): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
galaxy15_135L1.hasCarrierFrequency = true
|
||||
galaxy15_135L1.carrierFrequencyHz = 1575420000.0f
|
||||
galaxy15_135L1.carrierFrequencyHz = 1575420000.0
|
||||
galaxy15_135L1.sbasType = SbasType.WAAS
|
||||
return galaxy15_135L1
|
||||
}
|
||||
@@ -429,7 +429,7 @@ fun galaxy15_135L5(usedInFix: Boolean): SatelliteStatus {
|
||||
72f,
|
||||
25f);
|
||||
galaxy15_135L5.hasCarrierFrequency = true
|
||||
galaxy15_135L5.carrierFrequencyHz = 1176450000.0f
|
||||
galaxy15_135L5.carrierFrequencyHz = 1176450000.0
|
||||
galaxy15_135L5.sbasType = SbasType.WAAS
|
||||
return galaxy15_135L5
|
||||
}
|
||||
@@ -15,21 +15,22 @@
|
||||
*/
|
||||
package com.android.gpstest;
|
||||
|
||||
import static androidx.test.InstrumentationRegistry.getTargetContext;
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
|
||||
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
|
||||
|
||||
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;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import static androidx.test.InstrumentationRegistry.getTargetContext;
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
|
||||
@RunWith(AndroidJUnit4ClassRunner.class)
|
||||
public class UIUtilsAndroidTest {
|
||||
|
||||
@@ -38,13 +39,13 @@ public class UIUtilsAndroidTest {
|
||||
// Test German
|
||||
setLocale("de", "DE");
|
||||
|
||||
String dms = UIUtils.getDMSFromLocation(getTargetContext(), -42.853583, UIUtils.COORDINATE_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, UIUtils.COORDINATE_LATITUDE);
|
||||
dms = LibUIUtils.getDMSFromLocation(getTargetContext(), -42.853583, CoordinateType.LATITUDE);
|
||||
assertEquals("S\t\u200742° 51' 12.90\"", dms);
|
||||
}
|
||||
|
||||
@@ -53,13 +54,13 @@ public class UIUtilsAndroidTest {
|
||||
// Test German
|
||||
setLocale("de", "DE");
|
||||
|
||||
String dms = UIUtils.getDMSFromLocation(getTargetContext(), 47.64896, UIUtils.COORDINATE_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, UIUtils.COORDINATE_LONGITUDE);
|
||||
dms = LibUIUtils.getDMSFromLocation(getTargetContext(), 47.64896, CoordinateType.LONGITUDE);
|
||||
assertEquals("E\t047° 38' 56.26\"", dms);
|
||||
}
|
||||
|
||||
@@ -68,13 +69,13 @@ public class UIUtilsAndroidTest {
|
||||
// Test German
|
||||
setLocale("de", "DE");
|
||||
|
||||
String ddm = UIUtils.getDDMFromLocation(getTargetContext(), 24.15346, UIUtils.COORDINATE_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, UIUtils.COORDINATE_LATITUDE);
|
||||
ddm = LibUIUtils.getDDMFromLocation(getTargetContext(), 24.15346, CoordinateType.LATITUDE);
|
||||
assertEquals("N\t\u200724° 09.208", ddm);
|
||||
}
|
||||
|
||||
@@ -83,13 +84,13 @@ public class UIUtilsAndroidTest {
|
||||
// Test English
|
||||
setLocale("en", "US");
|
||||
|
||||
String ddm = UIUtils.getDDMFromLocation(getTargetContext(), -150.94523, UIUtils.COORDINATE_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, UIUtils.COORDINATE_LONGITUDE);
|
||||
ddm = LibUIUtils.getDDMFromLocation(getTargetContext(), -150.94523, CoordinateType.LONGITUDE);
|
||||
assertEquals("W\t150° 56,714", ddm);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,504 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2008-2013 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.
|
||||
*/
|
||||
|
||||
package com.android.gpstest;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Color;
|
||||
import android.location.GnssMeasurementsEvent;
|
||||
import android.location.GnssStatus;
|
||||
import android.location.GpsStatus;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.android.gpstest.map.MapViewModelController;
|
||||
import com.android.gpstest.map.OnMapClickListener;
|
||||
import com.android.gpstest.util.MapUtils;
|
||||
import com.android.gpstest.util.MathUtils;
|
||||
import com.google.android.gms.common.ConnectionResult;
|
||||
import com.google.android.gms.common.GoogleApiAvailability;
|
||||
import com.google.android.gms.maps.CameraUpdateFactory;
|
||||
import com.google.android.gms.maps.GoogleMap;
|
||||
import com.google.android.gms.maps.LocationSource;
|
||||
import com.google.android.gms.maps.OnMapReadyCallback;
|
||||
import com.google.android.gms.maps.SupportMapFragment;
|
||||
import com.google.android.gms.maps.model.CameraPosition;
|
||||
import com.google.android.gms.maps.model.LatLng;
|
||||
import com.google.android.gms.maps.model.LatLngBounds;
|
||||
import com.google.android.gms.maps.model.MapStyleOptions;
|
||||
import com.google.android.gms.maps.model.Marker;
|
||||
import com.google.android.gms.maps.model.MarkerOptions;
|
||||
import com.google.android.gms.maps.model.Polyline;
|
||||
import com.google.android.gms.maps.model.PolylineOptions;
|
||||
import com.google.maps.android.SphericalUtil;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static com.android.gpstest.map.MapConstants.ALLOW_GROUND_TRUTH_CHANGE;
|
||||
import static com.android.gpstest.map.MapConstants.CAMERA_ANCHOR_ZOOM;
|
||||
import static com.android.gpstest.map.MapConstants.CAMERA_INITIAL_BEARING;
|
||||
import static com.android.gpstest.map.MapConstants.CAMERA_INITIAL_TILT_ACCURACY;
|
||||
import static com.android.gpstest.map.MapConstants.CAMERA_INITIAL_TILT_MAP;
|
||||
import static com.android.gpstest.map.MapConstants.CAMERA_INITIAL_ZOOM;
|
||||
import static com.android.gpstest.map.MapConstants.CAMERA_MAX_TILT;
|
||||
import static com.android.gpstest.map.MapConstants.CAMERA_MIN_TILT;
|
||||
import static com.android.gpstest.map.MapConstants.DRAW_LINE_THRESHOLD_METERS;
|
||||
import static com.android.gpstest.map.MapConstants.GROUND_TRUTH;
|
||||
import static com.android.gpstest.map.MapConstants.MODE;
|
||||
import static com.android.gpstest.map.MapConstants.MODE_ACCURACY;
|
||||
import static com.android.gpstest.map.MapConstants.MODE_MAP;
|
||||
import static com.android.gpstest.map.MapConstants.MOVE_MAP_INTERACTION_THRESHOLD;
|
||||
import static com.android.gpstest.map.MapConstants.PREFERENCE_SHOWED_DIALOG;
|
||||
import static com.android.gpstest.map.MapConstants.TARGET_OFFSET_METERS;
|
||||
|
||||
public class GpsMapFragment extends SupportMapFragment
|
||||
implements GpsTestListener, View.OnClickListener, LocationSource,
|
||||
GoogleMap.OnCameraChangeListener, GoogleMap.OnMapClickListener,
|
||||
GoogleMap.OnMapLongClickListener,
|
||||
GoogleMap.OnMyLocationButtonClickListener, OnMapReadyCallback, MapViewModelController.MapInterface {
|
||||
|
||||
private Bundle mSavedInstanceState;
|
||||
|
||||
private GoogleMap mMap;
|
||||
|
||||
private LatLng mLatLng;
|
||||
|
||||
private OnLocationChangedListener mListener; //Used to update the map with new location
|
||||
|
||||
// Camera control
|
||||
private long mLastMapTouchTime = 0;
|
||||
|
||||
private CameraPosition mlastCameraPosition;
|
||||
|
||||
private boolean mGotFix;
|
||||
|
||||
// User preferences for map rotation and tilt based on sensors
|
||||
private boolean mRotate;
|
||||
|
||||
private boolean mTilt;
|
||||
|
||||
private OnMapClickListener mOnMapClickListener;
|
||||
|
||||
private Marker mGroundTruthMarker;
|
||||
|
||||
private Polyline mErrorLine;
|
||||
|
||||
private Location mLastLocation;
|
||||
|
||||
private List<Polyline> mPathLines = new ArrayList<>();
|
||||
|
||||
MapViewModelController mMapController;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View v = super.onCreateView(inflater, container, savedInstanceState);
|
||||
mLastLocation = null;
|
||||
|
||||
if (isGooglePlayServicesInstalled()) {
|
||||
// Save the savedInstanceState
|
||||
mSavedInstanceState = savedInstanceState;
|
||||
// Register for an async callback when the map is ready
|
||||
getMapAsync(this);
|
||||
} else {
|
||||
final SharedPreferences sp = Application.getPrefs();
|
||||
if (!sp.getBoolean(PREFERENCE_SHOWED_DIALOG, false)) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setMessage(getString(R.string.please_install_google_maps));
|
||||
builder.setPositiveButton(getString(R.string.install),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
sp.edit().putBoolean(PREFERENCE_SHOWED_DIALOG, true).commit();
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW,
|
||||
Uri.parse(
|
||||
"market://details?id=com.google.android.apps.maps"));
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
);
|
||||
builder.setNegativeButton(getString(R.string.no_thanks),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
sp.edit().putBoolean(PREFERENCE_SHOWED_DIALOG, true).commit();
|
||||
}
|
||||
}
|
||||
);
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
mMapController = new MapViewModelController(getActivity(), this);
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle bundle) {
|
||||
bundle.putString(MODE, mMapController.getMode());
|
||||
bundle.putBoolean(ALLOW_GROUND_TRUTH_CHANGE, mMapController.allowGroundTruthChange());
|
||||
if (mMapController.getGroundTruthLocation() != null) {
|
||||
bundle.putParcelable(GROUND_TRUTH, mMapController.getGroundTruthLocation());
|
||||
}
|
||||
super.onSaveInstanceState(bundle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
checkMapPreferences();
|
||||
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
public void onClick(View v) {
|
||||
}
|
||||
|
||||
public void gpsStart() {
|
||||
mGotFix = false;
|
||||
}
|
||||
|
||||
public void gpsStop() {
|
||||
}
|
||||
|
||||
public void onLocationChanged(Location loc) {
|
||||
//Update real-time location on map
|
||||
if (mListener != null) {
|
||||
mListener.onLocationChanged(loc);
|
||||
}
|
||||
|
||||
mLatLng = new LatLng(loc.getLatitude(), loc.getLongitude());
|
||||
|
||||
if (mMap != null) {
|
||||
//Get bounds for detection of real-time location within bounds
|
||||
LatLngBounds bounds = mMap.getProjection().getVisibleRegion().latLngBounds;
|
||||
if (!mGotFix &&
|
||||
(!bounds.contains(mLatLng) ||
|
||||
mMap.getCameraPosition().zoom < (mMap.getMaxZoomLevel() / 2))) {
|
||||
float tilt = mMapController.getMode().equals(MODE_MAP) ? CAMERA_INITIAL_TILT_MAP : CAMERA_INITIAL_TILT_ACCURACY;
|
||||
CameraPosition cameraPosition = new CameraPosition.Builder()
|
||||
.target(mLatLng)
|
||||
.zoom(CAMERA_INITIAL_ZOOM)
|
||||
.bearing(CAMERA_INITIAL_BEARING)
|
||||
.tilt(tilt)
|
||||
.build();
|
||||
|
||||
mMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
|
||||
}
|
||||
mGotFix = true;
|
||||
|
||||
if (mMapController.getMode().equals(MODE_ACCURACY) && !mMapController.allowGroundTruthChange() && mMapController.getGroundTruthLocation() != null) {
|
||||
// Draw error line between ground truth and calculated position
|
||||
LatLng gt = MapUtils.makeLatLng(mMapController.getGroundTruthLocation());
|
||||
LatLng current = MapUtils.makeLatLng(loc);
|
||||
|
||||
if (mErrorLine == null) {
|
||||
mErrorLine = mMap.addPolyline(new PolylineOptions()
|
||||
.add(gt, current)
|
||||
.color(Color.WHITE)
|
||||
.geodesic(true));
|
||||
} else {
|
||||
mErrorLine.setPoints(Arrays.asList(gt, current));
|
||||
}
|
||||
}
|
||||
if (mMapController.getMode().equals(MODE_ACCURACY) && mLastLocation != null) {
|
||||
// Draw line between this and last location
|
||||
boolean drawn = drawPathLine(mLastLocation, loc);
|
||||
if (drawn) {
|
||||
mLastLocation = loc;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mLastLocation == null) {
|
||||
mLastLocation = loc;
|
||||
}
|
||||
}
|
||||
|
||||
public void onStatusChanged(String provider, int status, Bundle extras) {
|
||||
}
|
||||
|
||||
public void onProviderEnabled(String provider) {
|
||||
}
|
||||
|
||||
public void onProviderDisabled(String provider) {
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void onGpsStatusChanged(int event, GpsStatus status) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssFirstFix(int ttffMillis) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssFixAcquired() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssFixLost() {
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@Override
|
||||
public void onSatelliteStatusChanged(GnssStatus status) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssStarted() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssStopped() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssMeasurementsReceived(GnssMeasurementsEvent event) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNmeaMessage(String message, long timestamp) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOrientationChanged(double orientation, double tilt) {
|
||||
// For performance reasons, only proceed if this fragment is visible
|
||||
if (!getUserVisibleHint()) {
|
||||
return;
|
||||
}
|
||||
// Only proceed if map is not null and we're in MAP mode
|
||||
if (mMap == null || !mMapController.getMode().equals(MODE_MAP)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
If we have a location fix, and we have a preference to rotate the map based on sensors,
|
||||
and the user hasn't touched the map lately, then do the map camera reposition
|
||||
*/
|
||||
if (mLatLng != null && mRotate
|
||||
&& System.currentTimeMillis() - mLastMapTouchTime
|
||||
> MOVE_MAP_INTERACTION_THRESHOLD) {
|
||||
|
||||
if (!mTilt || Double.isNaN(tilt)) {
|
||||
tilt = mlastCameraPosition != null ? mlastCameraPosition.tilt : 0;
|
||||
}
|
||||
|
||||
float clampedTilt = (float) MathUtils.clamp(CAMERA_MIN_TILT, tilt, CAMERA_MAX_TILT);
|
||||
|
||||
double offset = TARGET_OFFSET_METERS * (clampedTilt / CAMERA_MAX_TILT);
|
||||
|
||||
CameraPosition cameraPosition = CameraPosition.builder().
|
||||
tilt(clampedTilt).
|
||||
bearing((float) orientation).
|
||||
zoom((float) (CAMERA_ANCHOR_ZOOM + (tilt / CAMERA_MAX_TILT))).
|
||||
target(mTilt ? SphericalUtil.computeOffset(mLatLng, offset, orientation)
|
||||
: mLatLng).
|
||||
build();
|
||||
mMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps V2 Location updates
|
||||
*/
|
||||
@Override
|
||||
public void activate(OnLocationChangedListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps V2 Location updates
|
||||
*/
|
||||
@Override
|
||||
public void deactivate() {
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCameraChange(CameraPosition cameraPosition) {
|
||||
if (System.currentTimeMillis() - mLastMapTouchTime < MOVE_MAP_INTERACTION_THRESHOLD) {
|
||||
/*
|
||||
If the user recently interacted with the map (causing a camera change), extend the
|
||||
touch time before automatic map movements based on sensors will kick in
|
||||
*/
|
||||
mLastMapTouchTime = System.currentTimeMillis();
|
||||
}
|
||||
mlastCameraPosition = cameraPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMapClick(LatLng latLng) {
|
||||
mLastMapTouchTime = System.currentTimeMillis();
|
||||
if (!mMapController.getMode().equals(MODE_ACCURACY) || !mMapController.allowGroundTruthChange()) {
|
||||
// Don't allow changes to the ground truth location, so don't pass taps to listener
|
||||
return;
|
||||
}
|
||||
if (mMap != null) {
|
||||
addGroundTruthMarker(MapUtils.makeLocation(latLng));
|
||||
}
|
||||
|
||||
if (mOnMapClickListener != null) {
|
||||
Location location = new Location("OnMapClick");
|
||||
location.setLatitude(latLng.latitude);
|
||||
location.setLongitude(latLng.longitude);
|
||||
mOnMapClickListener.onMapClick(location);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addGroundTruthMarker(Location location) {
|
||||
if (mMap == null) {
|
||||
return;
|
||||
}
|
||||
LatLng latLng = MapUtils.makeLatLng(location);
|
||||
if (mGroundTruthMarker == null) {
|
||||
mGroundTruthMarker = mMap.addMarker(new MarkerOptions()
|
||||
.position(latLng)
|
||||
.title(Application.get().getString(R.string.ground_truth_marker_title)));
|
||||
} else {
|
||||
mGroundTruthMarker.setPosition(latLng);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMapLongClick(LatLng latLng) {
|
||||
mLastMapTouchTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMyLocationButtonClick() {
|
||||
mLastMapTouchTime = System.currentTimeMillis();
|
||||
// Return false, so button still functions as normal
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMapReady(GoogleMap googleMap) {
|
||||
mMap = googleMap;
|
||||
|
||||
mMapController.restoreState(mSavedInstanceState, getArguments(), mGroundTruthMarker == null);
|
||||
|
||||
checkMapPreferences();
|
||||
|
||||
// Show the location on the map
|
||||
try {
|
||||
mMap.setMyLocationEnabled(true);
|
||||
} catch (SecurityException e) {
|
||||
Log.e(mMapController.getMode(), "Tried to initialize my location on Google Map - " + e);
|
||||
}
|
||||
// Set location source
|
||||
mMap.setLocationSource(this);
|
||||
// Listener for camera changes
|
||||
mMap.setOnCameraChangeListener(this);
|
||||
// Listener for map / My Location button clicks, to disengage map camera control
|
||||
mMap.setOnMapClickListener(this);
|
||||
mMap.setOnMapLongClickListener(this);
|
||||
mMap.setOnMyLocationButtonClickListener(this);
|
||||
mMap.getUiSettings().setMapToolbarEnabled(false);
|
||||
|
||||
GpsTestActivity.getInstance().addListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if Google Play Services is available, false if it is not
|
||||
*/
|
||||
private static boolean isGooglePlayServicesInstalled() {
|
||||
return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(Application.get()) == ConnectionResult.SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener that should receive map click events
|
||||
* @param listener the listener that should receive map click events
|
||||
*/
|
||||
public void setOnMapClickListener(OnMapClickListener listener) {
|
||||
mOnMapClickListener = listener;
|
||||
}
|
||||
|
||||
private void checkMapPreferences() {
|
||||
SharedPreferences settings = Application.getPrefs();
|
||||
if (mMap != null && mMapController.getMode().equals(MODE_MAP)) {
|
||||
if (mMap.getMapType() != Integer.parseInt(
|
||||
settings.getString(getString(R.string.pref_key_map_type),
|
||||
String.valueOf(GoogleMap.MAP_TYPE_NORMAL))
|
||||
)) {
|
||||
mMap.setMapType(Integer.parseInt(
|
||||
settings.getString(getString(R.string.pref_key_map_type),
|
||||
String.valueOf(GoogleMap.MAP_TYPE_NORMAL))
|
||||
));
|
||||
}
|
||||
} else if (mMap != null && mMapController.getMode().equals(MODE_ACCURACY)) {
|
||||
mMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE);
|
||||
}
|
||||
if (mMapController.getMode().equals(MODE_MAP)) {
|
||||
mRotate = settings
|
||||
.getBoolean(getString(R.string.pref_key_rotate_map_with_compass), true);
|
||||
mTilt = settings.getBoolean(getString(R.string.pref_key_tilt_map_with_sensors), true);
|
||||
}
|
||||
|
||||
boolean useDarkTheme = Application.getPrefs().getBoolean(getString(R.string.pref_key_dark_theme), false);
|
||||
if (mMap != null && getActivity() != null && useDarkTheme) {
|
||||
mMap.setMapStyle(
|
||||
MapStyleOptions.loadRawResourceStyle(
|
||||
getActivity(), R.raw.dark_theme));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a line on the map between the two locations if its greater than a threshold value defined
|
||||
* by DRAW_LINE_THRESHOLD_METERS
|
||||
* @param loc1
|
||||
* @param loc2
|
||||
*/
|
||||
@Override
|
||||
public boolean drawPathLine(Location loc1, Location loc2) {
|
||||
if (loc1.distanceTo(loc2) < DRAW_LINE_THRESHOLD_METERS) {
|
||||
return false;
|
||||
}
|
||||
Polyline line = mMap.addPolyline(new PolylineOptions()
|
||||
.add(MapUtils.makeLatLng(loc1), MapUtils.makeLatLng(loc2))
|
||||
.color(Color.RED)
|
||||
.width(2.0f)
|
||||
.geodesic(true));
|
||||
mPathLines.add(line);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all path lines from the map
|
||||
*/
|
||||
@Override
|
||||
public void removePathLines() {
|
||||
for (Polyline line : mPathLines) {
|
||||
line.remove();
|
||||
}
|
||||
mPathLines = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
518
GPSTest/src/google/java/com/android/gpstest/ui/MapFragment.kt
Normal file
@@ -0,0 +1,518 @@
|
||||
/*
|
||||
* Copyright (C) 2008-2013 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.
|
||||
*/
|
||||
package com.android.gpstest.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Color
|
||||
import android.location.Location
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.Lifecycle
|
||||
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.library.data.LocationRepository
|
||||
import com.android.gpstest.library.util.MathUtils
|
||||
import com.android.gpstest.library.util.PreferenceUtil
|
||||
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.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import com.google.android.gms.maps.CameraUpdateFactory
|
||||
import com.google.android.gms.maps.GoogleMap
|
||||
import com.google.android.gms.maps.GoogleMap.OnCameraChangeListener
|
||||
import com.google.android.gms.maps.GoogleMap.OnMapLongClickListener
|
||||
import com.google.android.gms.maps.GoogleMap.OnMyLocationButtonClickListener
|
||||
import com.google.android.gms.maps.LocationSource
|
||||
import com.google.android.gms.maps.LocationSource.OnLocationChangedListener
|
||||
import com.google.android.gms.maps.SupportMapFragment
|
||||
import com.google.android.gms.maps.model.CameraPosition
|
||||
import com.google.android.gms.maps.model.LatLng
|
||||
import com.google.android.gms.maps.model.MapStyleOptions
|
||||
import com.google.android.gms.maps.model.Marker
|
||||
import com.google.android.gms.maps.model.MarkerOptions
|
||||
import com.google.android.gms.maps.model.Polyline
|
||||
import com.google.android.gms.maps.model.PolylineOptions
|
||||
import com.google.maps.android.SphericalUtil
|
||||
import com.google.maps.android.ktx.awaitMap
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MapFragment : SupportMapFragment(), View.OnClickListener, LocationSource,
|
||||
OnCameraChangeListener, GoogleMap.OnMapClickListener, OnMapLongClickListener,
|
||||
OnMyLocationButtonClickListener, MapInterface {
|
||||
private var savedInstanceState: Bundle? = null
|
||||
private var map: GoogleMap? = null
|
||||
private var latLng: LatLng? = null
|
||||
private var listener //Used to update the map with new location
|
||||
: OnLocationChangedListener? = null
|
||||
|
||||
// Camera control
|
||||
private var lastMapTouchTime: Long = 0
|
||||
private var lastCameraPosition: CameraPosition? = null
|
||||
private var gotFix = false
|
||||
|
||||
// User preferences for map rotation and tilt based on sensors
|
||||
private var rotate = false
|
||||
private var tiltEnabled = false
|
||||
private var onMapClickListener: OnMapClickListener? = null
|
||||
private var groundTruthMarker: Marker? = null
|
||||
private var errorLine: Polyline? = null
|
||||
private var lastLocation: Location? = null
|
||||
private var pathLines: MutableList<Polyline> = ArrayList()
|
||||
var mapController: MapViewModelController? = null
|
||||
|
||||
// Repository of location data that the service will observe, injected via Hilt
|
||||
@Inject
|
||||
lateinit var repository: LocationRepository
|
||||
|
||||
// Get a reference to the Job from the Flow so we can stop it from UI events
|
||||
private var locationFlow: Job? = null
|
||||
private var sensorFlow: Job? = null
|
||||
|
||||
// Preference listener that will cancel the above flows when the user turns off tracking via UI
|
||||
private val trackingListener: SharedPreferences.OnSharedPreferenceChangeListener =
|
||||
PreferenceUtil.newStopTrackingListener ({ onGnssStopped() }, prefs)
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val v = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
lastLocation = null
|
||||
|
||||
Application.prefs.registerOnSharedPreferenceChangeListener(trackingListener)
|
||||
|
||||
if (isGooglePlayServicesInstalled) {
|
||||
// Save the savedInstanceState
|
||||
this.savedInstanceState = savedInstanceState
|
||||
val mapFragment = this
|
||||
lifecycle.coroutineScope.launchWhenCreated {
|
||||
val googleMap = awaitMap()
|
||||
setupMap(mapFragment, googleMap)
|
||||
observeLocationUpdateStates()
|
||||
}
|
||||
} else {
|
||||
val sp = Application.prefs
|
||||
if (!sp.getBoolean(MapConstants.PREFERENCE_SHOWED_DIALOG, false)) {
|
||||
val builder = AlertDialog.Builder(
|
||||
requireActivity()
|
||||
)
|
||||
builder.setMessage(getString(R.string.please_install_google_maps))
|
||||
builder.setPositiveButton(
|
||||
getString(R.string.install)
|
||||
) { _, _ ->
|
||||
sp.edit().putBoolean(MapConstants.PREFERENCE_SHOWED_DIALOG, true).apply()
|
||||
val intent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(
|
||||
"market://details?id=com.google.android.apps.maps"
|
||||
)
|
||||
)
|
||||
startActivity(intent)
|
||||
}
|
||||
builder.setNegativeButton(
|
||||
getString(R.string.no_thanks)
|
||||
) { _, _ ->
|
||||
sp.edit().putBoolean(MapConstants.PREFERENCE_SHOWED_DIALOG, true).apply()
|
||||
}
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
mapController = MapViewModelController(activity, this)
|
||||
return v
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(bundle: Bundle) {
|
||||
bundle.putString(MapConstants.MODE, mapController!!.mode)
|
||||
bundle.putBoolean(
|
||||
MapConstants.ALLOW_GROUND_TRUTH_CHANGE,
|
||||
mapController!!.allowGroundTruthChange()
|
||||
)
|
||||
if (mapController!!.groundTruthLocation != null) {
|
||||
bundle.putParcelable(MapConstants.GROUND_TRUTH, mapController!!.groundTruthLocation)
|
||||
}
|
||||
super.onSaveInstanceState(bundle)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
checkMapPreferences()
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {}
|
||||
|
||||
private fun setupMap(mapFragment: MapFragment, googleMap: GoogleMap) {
|
||||
map = googleMap
|
||||
mapController!!.restoreState(savedInstanceState, arguments, groundTruthMarker == null)
|
||||
checkMapPreferences()
|
||||
|
||||
// Show the location on the map
|
||||
try {
|
||||
googleMap.isMyLocationEnabled = true
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(mapController!!.mode, "Tried to initialize my location on Google Map - $e")
|
||||
}
|
||||
// Set location source
|
||||
googleMap.setLocationSource(mapFragment)
|
||||
// Listener for camera changes
|
||||
googleMap.setOnCameraChangeListener(mapFragment)
|
||||
// Listener for map / My Location button clicks, to disengage map camera control
|
||||
googleMap.setOnMapClickListener(mapFragment)
|
||||
googleMap.setOnMapLongClickListener(mapFragment)
|
||||
googleMap.setOnMyLocationButtonClickListener(mapFragment)
|
||||
googleMap.uiSettings.isMapToolbarEnabled = false
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeLocationUpdateStates() {
|
||||
repository.receivingLocationUpdates
|
||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
||||
.onEach {
|
||||
when (it) {
|
||||
true -> onGnssStarted()
|
||||
false -> onGnssStopped()
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun onGnssStarted() {
|
||||
gotFix = false
|
||||
observeFlows()
|
||||
}
|
||||
|
||||
private fun onGnssStopped() {
|
||||
cancelFlows()
|
||||
}
|
||||
|
||||
private fun cancelFlows() {
|
||||
// Cancel updates (Note that these are canceled via trackingListener preference listener
|
||||
// in the case where updates are stopped from the Activity UI switch.
|
||||
locationFlow?.cancel()
|
||||
sensorFlow?.cancel()
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeFlows() {
|
||||
observeLocationFlow()
|
||||
observeSensorFlow()
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeLocationFlow() {
|
||||
if (locationFlow?.isActive == true) {
|
||||
// If we're already observing updates, don't register again
|
||||
return
|
||||
}
|
||||
// Observe locations via Flow as they are generated by the repository
|
||||
locationFlow = repository.getLocations()
|
||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
||||
.onEach {
|
||||
//Log.d(GpsStatusFragment.TAG, "Map location: ${it.toNotificationTitle()}")
|
||||
onLocationChanged(it)
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeSensorFlow() {
|
||||
if (sensorFlow?.isActive == true) {
|
||||
// If we're already observing updates, don't register again
|
||||
return
|
||||
}
|
||||
// Observe locations via Flow as they are generated by the repository
|
||||
sensorFlow = repository.getSensorUpdates()
|
||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
||||
.onEach {
|
||||
//Log.d(TAG, "Map sensor: orientation ${it.values[0]}, tilt ${it.values[1]}")
|
||||
onOrientationChanged(it.values[0], it.values[1])
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private fun onLocationChanged(loc: Location) {
|
||||
// Update real-time location on map
|
||||
listener?.onLocationChanged(loc)
|
||||
|
||||
val latLng = LatLng(loc.latitude, loc.longitude)
|
||||
this.latLng = latLng
|
||||
val googleMap = map
|
||||
if (googleMap != null) {
|
||||
// Get bounds for detection of real-time location within bounds
|
||||
val bounds = googleMap.projection.visibleRegion.latLngBounds
|
||||
if (!gotFix &&
|
||||
(!bounds.contains(latLng) ||
|
||||
googleMap.cameraPosition.zoom < googleMap.maxZoomLevel / 2)
|
||||
) {
|
||||
val tilt =
|
||||
if (mapController!!.mode == MapConstants.MODE_MAP) MapConstants.CAMERA_INITIAL_TILT_MAP else MapConstants.CAMERA_INITIAL_TILT_ACCURACY
|
||||
val cameraPosition = CameraPosition.Builder()
|
||||
.target(latLng)
|
||||
.zoom(MapConstants.CAMERA_INITIAL_ZOOM)
|
||||
.bearing(MapConstants.CAMERA_INITIAL_BEARING)
|
||||
.tilt(tilt)
|
||||
.build()
|
||||
googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition))
|
||||
}
|
||||
gotFix = true
|
||||
if (mapController!!.mode == MapConstants.MODE_ACCURACY && !mapController!!.allowGroundTruthChange() && mapController!!.groundTruthLocation != null) {
|
||||
// Draw error line between ground truth and calculated position
|
||||
val gt = MapUtils.makeLatLng(mapController!!.groundTruthLocation)
|
||||
val current = MapUtils.makeLatLng(loc)
|
||||
if (errorLine == null) {
|
||||
errorLine = googleMap.addPolyline(
|
||||
PolylineOptions()
|
||||
.add(gt, current)
|
||||
.color(Color.WHITE)
|
||||
.geodesic(true)
|
||||
)
|
||||
} else {
|
||||
errorLine!!.points = listOf(gt, current)
|
||||
}
|
||||
}
|
||||
if (mapController!!.mode == MapConstants.MODE_ACCURACY && lastLocation != null) {
|
||||
// Draw line between this and last location
|
||||
val drawn = drawPathLine(lastLocation!!, loc)
|
||||
if (drawn) {
|
||||
lastLocation = loc
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lastLocation == null) {
|
||||
lastLocation = loc
|
||||
}
|
||||
}
|
||||
|
||||
private fun onOrientationChanged(orientation: Double, tilt: Double) {
|
||||
// For performance reasons, only proceed if this fragment is visible
|
||||
if (!userVisibleHint) {
|
||||
return
|
||||
}
|
||||
// Only proceed if map is not null and we're in MAP mode
|
||||
if (map == null || mapController!!.mode != MapConstants.MODE_MAP) {
|
||||
return
|
||||
}
|
||||
var mutableTilt = tilt
|
||||
|
||||
/*
|
||||
If we have a location fix, and we have a preference to rotate the map based on sensors,
|
||||
and the user hasn't touched the map lately, then do the map camera reposition
|
||||
*/
|
||||
if (latLng != null && rotate
|
||||
&& (System.currentTimeMillis() - lastMapTouchTime
|
||||
> MapConstants.MOVE_MAP_INTERACTION_THRESHOLD)
|
||||
) {
|
||||
if (!tiltEnabled || java.lang.Double.isNaN(mutableTilt)) {
|
||||
mutableTilt =
|
||||
if (lastCameraPosition != null) lastCameraPosition!!.tilt.toDouble() else 0.toDouble()
|
||||
}
|
||||
val clampedTilt = MathUtils.clamp(
|
||||
MapConstants.CAMERA_MIN_TILT.toDouble(),
|
||||
mutableTilt,
|
||||
MapConstants.CAMERA_MAX_TILT.toDouble()
|
||||
).toFloat()
|
||||
val offset =
|
||||
MapConstants.TARGET_OFFSET_METERS * (clampedTilt / MapConstants.CAMERA_MAX_TILT)
|
||||
val cameraPosition = CameraPosition.builder().tilt(clampedTilt).bearing(
|
||||
orientation.toFloat()
|
||||
)
|
||||
.zoom((MapConstants.CAMERA_ANCHOR_ZOOM + mutableTilt / MapConstants.CAMERA_MAX_TILT).toFloat())
|
||||
.target(
|
||||
if (tiltEnabled) SphericalUtil.computeOffset(
|
||||
latLng,
|
||||
offset,
|
||||
orientation
|
||||
) else latLng
|
||||
).build()
|
||||
map!!.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps V2 Location updates
|
||||
*/
|
||||
override fun activate(listener: OnLocationChangedListener) {
|
||||
this.listener = listener
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps V2 Location updates
|
||||
*/
|
||||
override fun deactivate() {
|
||||
listener = null
|
||||
}
|
||||
|
||||
override fun onCameraChange(cameraPosition: CameraPosition) {
|
||||
if (System.currentTimeMillis() - lastMapTouchTime < MapConstants.MOVE_MAP_INTERACTION_THRESHOLD) {
|
||||
/*
|
||||
If the user recently interacted with the map (causing a camera change), extend the
|
||||
touch time before automatic map movements based on sensors will kick in
|
||||
*/
|
||||
lastMapTouchTime = System.currentTimeMillis()
|
||||
}
|
||||
lastCameraPosition = cameraPosition
|
||||
}
|
||||
|
||||
override fun onMapClick(latLng: LatLng) {
|
||||
lastMapTouchTime = System.currentTimeMillis()
|
||||
if (mapController!!.mode != MapConstants.MODE_ACCURACY || !mapController!!.allowGroundTruthChange()) {
|
||||
// Don't allow changes to the ground truth location, so don't pass taps to listener
|
||||
return
|
||||
}
|
||||
if (map != null) {
|
||||
addGroundTruthMarker(MapUtils.makeLocation(latLng))
|
||||
}
|
||||
if (onMapClickListener != null) {
|
||||
val location = Location("OnMapClick")
|
||||
location.latitude = latLng.latitude
|
||||
location.longitude = latLng.longitude
|
||||
onMapClickListener!!.onMapClick(location)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addGroundTruthMarker(location: Location) {
|
||||
if (map == null) {
|
||||
return
|
||||
}
|
||||
val latLng = MapUtils.makeLatLng(location)
|
||||
if (groundTruthMarker == null) {
|
||||
groundTruthMarker = map!!.addMarker(
|
||||
MarkerOptions()
|
||||
.position(latLng)
|
||||
.title(Application.app.getString(R.string.ground_truth_marker_title))
|
||||
)
|
||||
} else {
|
||||
groundTruthMarker!!.position = latLng
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMapLongClick(latLng: LatLng) {
|
||||
lastMapTouchTime = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
override fun onMyLocationButtonClick(): Boolean {
|
||||
lastMapTouchTime = System.currentTimeMillis()
|
||||
// Return false, so button still functions as normal
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener that should receive map click events
|
||||
* @param listener the listener that should receive map click events
|
||||
*/
|
||||
fun setOnMapClickListener(listener: OnMapClickListener?) {
|
||||
onMapClickListener = listener
|
||||
}
|
||||
|
||||
private fun checkMapPreferences() {
|
||||
val settings = Application.prefs
|
||||
if (map != null && mapController!!.mode == MapConstants.MODE_MAP) {
|
||||
if (map!!.mapType !=
|
||||
settings.getString(
|
||||
getString(R.string.pref_key_map_type),
|
||||
GoogleMap.MAP_TYPE_NORMAL.toString()
|
||||
)
|
||||
?.toInt() ?: GoogleMap.MAP_TYPE_NORMAL
|
||||
) {
|
||||
map!!.mapType = settings.getString(
|
||||
getString(R.string.pref_key_map_type),
|
||||
GoogleMap.MAP_TYPE_NORMAL.toString()
|
||||
)
|
||||
?.toInt() ?: GoogleMap.MAP_TYPE_NORMAL
|
||||
}
|
||||
} else if (map != null && mapController!!.mode == MapConstants.MODE_ACCURACY) {
|
||||
map!!.mapType = GoogleMap.MAP_TYPE_SATELLITE
|
||||
}
|
||||
if (mapController!!.mode == MapConstants.MODE_MAP) {
|
||||
rotate = settings
|
||||
.getBoolean(getString(R.string.pref_key_rotate_map_with_compass), true)
|
||||
tiltEnabled = settings.getBoolean(getString(R.string.pref_key_tilt_map_with_sensors), true)
|
||||
}
|
||||
val useDarkTheme =
|
||||
Application.prefs.getBoolean(getString(R.string.pref_key_dark_theme), false)
|
||||
if (map != null && activity != null && useDarkTheme) {
|
||||
map!!.setMapStyle(
|
||||
MapStyleOptions.loadRawResourceStyle(
|
||||
activity, R.raw.dark_theme
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a line on the map between the two locations if its greater than a threshold value defined
|
||||
* by DRAW_LINE_THRESHOLD_METERS
|
||||
* @param loc1
|
||||
* @param loc2
|
||||
*/
|
||||
override fun drawPathLine(loc1: Location, loc2: Location): Boolean {
|
||||
if (loc1.distanceTo(loc2) < MapConstants.DRAW_LINE_THRESHOLD_METERS) {
|
||||
return false
|
||||
}
|
||||
val line = map!!.addPolyline(
|
||||
PolylineOptions()
|
||||
.add(MapUtils.makeLatLng(loc1), MapUtils.makeLatLng(loc2))
|
||||
.color(Color.RED)
|
||||
.width(2.0f)
|
||||
.geodesic(true)
|
||||
)
|
||||
pathLines.add(line)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all path lines from the map
|
||||
*/
|
||||
override fun removePathLines() {
|
||||
for (line in pathLines) {
|
||||
line.remove()
|
||||
}
|
||||
pathLines = ArrayList()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "GpsMapFragment"
|
||||
|
||||
/**
|
||||
* Returns true if Google Play Services is available, false if it is not
|
||||
*/
|
||||
private val isGooglePlayServicesInstalled: Boolean
|
||||
get() = GoogleApiAvailability.getInstance()
|
||||
.isGooglePlayServicesAvailable(Application.app) == ConnectionResult.SUCCESS
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public class BuildUtils {
|
||||
* @return the Google Play Services app version as well as the Google Play Services library version
|
||||
*/
|
||||
public static String getPlayServicesVersion() {
|
||||
PackageManager pm = Application.get().getPackageManager();
|
||||
PackageManager pm = Application.Companion.getApp().getPackageManager();
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
PackageInfo appInfoPlayServices;
|
||||
|
||||
@@ -28,7 +28,7 @@ public class MapUtils {
|
||||
* @param lon The longitude.
|
||||
* @return A LatLng representing this latitude/longitude.
|
||||
*/
|
||||
public static final LatLng makeLatLng(double lat, double lon) {
|
||||
public static LatLng makeLatLng(double lat, double lon) {
|
||||
return new LatLng(lat, lon);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public class MapUtils {
|
||||
* @param l Location to convert
|
||||
* @return A LatLng representing this Location.
|
||||
*/
|
||||
public static final LatLng makeLatLng(Location l) {
|
||||
public static LatLng makeLatLng(Location l) {
|
||||
return makeLatLng(l.getLatitude(), l.getLongitude());
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ public class MapUtils {
|
||||
* @param latLng LatLng to convert
|
||||
* @return A Location representing this LatLng.
|
||||
*/
|
||||
public static final Location makeLocation(LatLng latLng) {
|
||||
public static Location makeLocation(LatLng latLng) {
|
||||
Location l = new Location("FromLatLng");
|
||||
l.setLatitude(latLng.latitude);
|
||||
l.setLongitude(latLng.longitude);
|
||||
|
||||
@@ -18,9 +18,18 @@
|
||||
package="com.android.gpstest">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<!-- Required for foreground services on P+ -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<!-- Required for notifications on T+ -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- Required for the location service when targeting API 34 and up -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
|
||||
<application
|
||||
android:name=".Application"
|
||||
@@ -30,9 +39,10 @@
|
||||
android:requestLegacyExternalStorage="true">
|
||||
|
||||
<activity
|
||||
android:name=".GpsTestActivity"
|
||||
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" />
|
||||
@@ -46,8 +56,10 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".Preferences"
|
||||
android:label="@string/pref_title">
|
||||
android:name=".ui.Preferences"
|
||||
android:label="@string/pref_title"
|
||||
android:exported="true"
|
||||
android:noHistory="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -55,12 +67,12 @@
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".HelpActivity"
|
||||
android:name=".ui.HelpActivity"
|
||||
android:label="@string/title_help"
|
||||
android:parentActivityName=".GpsTestActivity">
|
||||
android:parentActivityName=".ui.MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.android.gpstest.GpsTestActivity" />
|
||||
android:value="com.android.gpstest.ui.MainActivity" />
|
||||
</activity>
|
||||
<!-- For sending the log file -->
|
||||
<provider
|
||||
@@ -72,5 +84,10 @@
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_providers_paths" />
|
||||
</provider>
|
||||
<service
|
||||
android:name=".ForegroundOnlyLocationService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="location" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.multidex.MultiDexApplication;
|
||||
|
||||
import com.android.gpstest.lang.LocaleManager;
|
||||
|
||||
/**
|
||||
* Holds application-wide state
|
||||
*
|
||||
* @author Sean J. Barbeau
|
||||
*/
|
||||
public class Application extends MultiDexApplication {
|
||||
|
||||
private static Application mApp;
|
||||
|
||||
private SharedPreferences mPrefs;
|
||||
|
||||
public static Application get() {
|
||||
return mApp;
|
||||
}
|
||||
|
||||
public static SharedPreferences getPrefs() {
|
||||
return get().mPrefs;
|
||||
}
|
||||
|
||||
private static LocaleManager mLocaleManager;
|
||||
|
||||
public static LocaleManager getLocaleManager() {
|
||||
return mLocaleManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
mApp = this;
|
||||
mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
|
||||
// Set theme
|
||||
if (Application.getPrefs().getBoolean(getString(R.string.pref_key_dark_theme), false)) {
|
||||
setTheme(R.style.AppTheme_Dark);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
mApp = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
mLocaleManager = new LocaleManager(base);
|
||||
super.attachBaseContext(mLocaleManager.setLocale(base));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
mLocaleManager.setLocale(this);
|
||||
}
|
||||
}
|
||||
64
GPSTest/src/main/java/com/android/gpstest/Application.kt
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Configuration
|
||||
import android.preference.PreferenceManager
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import com.android.gpstest.lang.LocaleManager
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
/**
|
||||
* Holds application-wide state
|
||||
*
|
||||
* @author Sean J. Barbeau
|
||||
*/
|
||||
@HiltAndroidApp
|
||||
class Application : MultiDexApplication() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
app = this
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
// Set theme
|
||||
if (prefs.getBoolean(getString(R.string.pref_key_dark_theme), false)) {
|
||||
setTheme(R.style.AppTheme_Dark)
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
localeManager = LocaleManager(base)
|
||||
super.attachBaseContext(localeManager.setLocale(base))
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
localeManager.setLocale(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var app: Application
|
||||
private set
|
||||
|
||||
lateinit var localeManager: LocaleManager
|
||||
private set
|
||||
lateinit var prefs: SharedPreferences
|
||||
private set
|
||||
}
|
||||
}
|
||||
@@ -1,383 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.android.gpstest.model.ConstellationFamily;
|
||||
import com.android.gpstest.model.GnssType;
|
||||
import com.android.gpstest.model.Satellite;
|
||||
import com.android.gpstest.model.SatelliteMetadata;
|
||||
import com.android.gpstest.model.SatelliteStatus;
|
||||
import com.android.gpstest.model.SbasType;
|
||||
import com.android.gpstest.util.CarrierFreqUtils;
|
||||
import com.android.gpstest.util.SatelliteUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.android.gpstest.model.SatelliteStatus.NO_DATA;
|
||||
import static com.android.gpstest.util.CarrierFreqUtils.CF_UNKNOWN;
|
||||
import static com.android.gpstest.util.CarrierFreqUtils.CF_UNSUPPORTED;
|
||||
|
||||
/**
|
||||
* View model that holds device properties
|
||||
*/
|
||||
public class DeviceInfoViewModel extends AndroidViewModel {
|
||||
|
||||
private MutableLiveData<Map<String, Satellite>> mGnssSatellites = new MutableLiveData<>();
|
||||
|
||||
private MutableLiveData<Map<String, Satellite>> mSbasSatellites = new MutableLiveData<>();
|
||||
|
||||
private boolean mIsDualFrequencyPerSatInView = false;
|
||||
|
||||
private boolean mIsDualFrequencyPerSatInUse = false;
|
||||
|
||||
private boolean mIsNonPrimaryCarrierFreqInView = false;
|
||||
|
||||
private boolean mIsNonPrimaryCarrierFreqInUse = false;
|
||||
|
||||
private boolean gotFirstFix = false;
|
||||
|
||||
private Set<GnssType> supportedGnss = new HashSet<>();
|
||||
|
||||
private Set<SbasType> supportedSbas = new HashSet<>();
|
||||
|
||||
private Set<String> supportedGnssCfs = new HashSet<>();
|
||||
|
||||
private Set<String> supportedSbasCfs = new HashSet<>();
|
||||
|
||||
/**
|
||||
* A set of metadata about all satellites the device knows of
|
||||
*/
|
||||
private MutableLiveData<SatelliteMetadata> mSatelliteMetadata = new MutableLiveData<>();
|
||||
|
||||
/**
|
||||
* Map of status keys (created using SatelliteUtils.createGnssStatusKey()) to the status that
|
||||
* has been detected as having duplicate carrier frequency data with another signal
|
||||
*/
|
||||
private Map<String, SatelliteStatus> mDuplicateCarrierStatuses = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Map of status keys (created using SatelliteUtils.createGnssStatusKey()) to the status that
|
||||
* has been detected with an unknown GNSS frequency
|
||||
*/
|
||||
private Map<String, SatelliteStatus> mUnknownCarrierStatuses = new HashMap<>();
|
||||
|
||||
public DeviceInfoViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
}
|
||||
|
||||
public MutableLiveData<Map<String, Satellite>> getGnssSatellites() {
|
||||
return mGnssSatellites;
|
||||
}
|
||||
|
||||
public MutableLiveData<Map<String, Satellite>> getSbasSatellites() {
|
||||
return mSbasSatellites;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map of status keys (created using SatelliteUtils.createGnssStatusKey()) to the status that
|
||||
* has been detected as having duplicate carrier frequency data with another signal
|
||||
*
|
||||
* @return a map of status keys (created using SatelliteUtils.createGnssStatusKey()) to the status that
|
||||
* has been detected as having duplicate carrier frequency data with another signal
|
||||
*/
|
||||
public Map<String, SatelliteStatus> getDuplicateCarrierStatuses() {
|
||||
return mDuplicateCarrierStatuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map of status keys (created using SatelliteUtils.createGnssStatusKey()) to the status that
|
||||
* has been detected with an unknown GNSS frequency
|
||||
*
|
||||
* @return a map of status keys (created using SatelliteUtils.createGnssStatusKey()) to the status that
|
||||
* has been detected with an unknown GNSS frequency
|
||||
*/
|
||||
public Map<String, SatelliteStatus> getUnknownCarrierStatuses() {
|
||||
return mUnknownCarrierStatuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this device is viewing multiple signals from the same satellite, false if it is not
|
||||
*
|
||||
* @return true if this device is viewing multiple signals from the same satellite, false if it is not
|
||||
*/
|
||||
public boolean isDualFrequencyPerSatInView() {
|
||||
return mIsDualFrequencyPerSatInView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this device is using multiple signals from the same satellite, false if it is not
|
||||
*
|
||||
* @return true if this device is using multiple signals from the same satellite, false if it is not
|
||||
*/
|
||||
public boolean isDualFrequencyPerSatInUse() {
|
||||
return mIsDualFrequencyPerSatInUse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a non-primary carrier frequency is in view by at least one satellite, or false if
|
||||
* only primary carrier frequencies are in view
|
||||
*
|
||||
* @return true if a non-primary carrier frequency is in use by at least one satellite, or false if
|
||||
* only primary carrier frequencies are in view
|
||||
*/
|
||||
public boolean isNonPrimaryCarrierFreqInView() {
|
||||
return mIsNonPrimaryCarrierFreqInView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a non-primary carrier frequency is in use by at least one satellite, or false if
|
||||
* only primary carrier frequencies are in use
|
||||
*
|
||||
* @return true if a non-primary carrier frequency is in use by at least one satellite, or false if
|
||||
* only primary carrier frequencies are in use
|
||||
*/
|
||||
public boolean isNonPrimaryCarrierFreqInUse() {
|
||||
return mIsNonPrimaryCarrierFreqInUse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the metadata about a group of satellites
|
||||
*
|
||||
* @return the metadata about a group of satellites
|
||||
*/
|
||||
public MutableLiveData<SatelliteMetadata> getSatelliteMetadata() {
|
||||
return mSatelliteMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of GNSS types that are supported by the device
|
||||
* @return a set of GNSS types that are supported by the device
|
||||
*/
|
||||
public Set<GnssType> getSupportedGnss() {
|
||||
return supportedGnss;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of SBAS types that are supported by the device
|
||||
* @return a set of SBAS types that are supported by the device
|
||||
*/
|
||||
public Set<SbasType> getSupportedSbas() {
|
||||
return supportedSbas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of GNSS carrier frequency labels that are supported by the device
|
||||
* @return a set of GNSS carrier frequency labels that are supported by the device
|
||||
*/
|
||||
public Set<String> getSupportedGnssCfs() {
|
||||
return supportedGnssCfs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of SBAS carrier frequency labels that are supported by the device
|
||||
* @return a set of SBAS carrier frequency labels that are supported by the device
|
||||
*/
|
||||
public Set<String> getSupportedSbasCfs() {
|
||||
return supportedSbasCfs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this view model has observed a GNSS fix first, false if it has not
|
||||
* @return true if this view model has observed a GNSS fix first, false if it has not
|
||||
*/
|
||||
public boolean gotFirstFix() {
|
||||
return gotFirstFix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets if the view model has observed a first GNSS fix during this execution
|
||||
* @param value true if the model has observed a first GNSS fix during this execution, false if it has not
|
||||
*/
|
||||
public void setGotFirstFix(boolean value) {
|
||||
gotFirstFix = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new set of GNSS and SBAS status objects (signals) so they can be analyzed and grouped
|
||||
* into satellites
|
||||
*
|
||||
* @param gnssStatuses a new set of GNSS status objects (signals)
|
||||
* @param sbasStatuses a new set of SBAS status objects (signals)
|
||||
*/
|
||||
public void setStatuses(List<SatelliteStatus> gnssStatuses, List<SatelliteStatus> sbasStatuses) {
|
||||
ConstellationFamily gnssSatellites = getSatellitesFromStatuses(gnssStatuses);
|
||||
ConstellationFamily sbasSatellites = getSatellitesFromStatuses(sbasStatuses);
|
||||
|
||||
mGnssSatellites.setValue(gnssSatellites.getSatellites());
|
||||
mSbasSatellites.setValue(sbasSatellites.getSatellites());
|
||||
|
||||
int numSignalsUsed = gnssSatellites.getSatelliteMetadata().getNumSignalsUsed() + sbasSatellites.getSatelliteMetadata().getNumSignalsUsed();
|
||||
int numSignalsInView = gnssSatellites.getSatelliteMetadata().getNumSignalsInView() + sbasSatellites.getSatelliteMetadata().getNumSignalsInView();
|
||||
int numSignalsTotal = gnssSatellites.getSatelliteMetadata().getNumSignalsTotal() + sbasSatellites.getSatelliteMetadata().getNumSignalsTotal();
|
||||
|
||||
|
||||
int numSatsUsed = gnssSatellites.getSatelliteMetadata().getNumSatsUsed() + sbasSatellites.getSatelliteMetadata().getNumSatsUsed();
|
||||
int numSatsInView = gnssSatellites.getSatelliteMetadata().getNumSatsInView() + sbasSatellites.getSatelliteMetadata().getNumSatsInView();
|
||||
int numSatsTotal = gnssSatellites.getSatelliteMetadata().getNumSatsTotal() + sbasSatellites.getSatelliteMetadata().getNumSatsTotal();
|
||||
mSatelliteMetadata.setValue(new SatelliteMetadata(numSignalsInView, numSignalsUsed, numSignalsTotal, numSatsInView, numSatsUsed, numSatsTotal));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map with the provided status grouped into satellites
|
||||
* @param allStatuses all statuses for either all GNSS or SBAS constellations
|
||||
* @return a map with the provided status grouped into satellites. The key to the map is the combination of constellation and ID
|
||||
* created using SatelliteUtils.createGnssSatelliteKey().
|
||||
*/
|
||||
private ConstellationFamily getSatellitesFromStatuses(List<SatelliteStatus> allStatuses) {
|
||||
Map<String, Satellite> satellites = new HashMap<>();
|
||||
int numSignalsUsed = 0;
|
||||
int numSignalsInView = 0;
|
||||
int numSatsUsed = 0;
|
||||
int numSatsInView = 0;
|
||||
|
||||
if (allStatuses == null) {
|
||||
return new ConstellationFamily(satellites, new SatelliteMetadata(0, 0, 0, 0, 0, 0));
|
||||
}
|
||||
|
||||
for (SatelliteStatus s : allStatuses) {
|
||||
if (s.getUsedInFix()) {
|
||||
numSignalsUsed++;
|
||||
}
|
||||
if (s.getCn0DbHz() != NO_DATA) {
|
||||
numSignalsInView++;
|
||||
}
|
||||
|
||||
// Save the supported GNSS or SBAS type
|
||||
String key = SatelliteUtils.createGnssSatelliteKey(s);
|
||||
if (s.getGnssType() != GnssType.UNKNOWN) {
|
||||
if (s.getGnssType() != GnssType.SBAS) {
|
||||
supportedGnss.add(s.getGnssType());
|
||||
} else {
|
||||
if (s.getSbasType() != SbasType.UNKNOWN) {
|
||||
supportedSbas.add(s.getSbasType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get carrier label
|
||||
String carrierLabel = CarrierFreqUtils.getCarrierFrequencyLabel(s);
|
||||
if (carrierLabel.equals(CF_UNKNOWN)) {
|
||||
mUnknownCarrierStatuses.put(SatelliteUtils.createGnssStatusKey(s), s);
|
||||
}
|
||||
if (!carrierLabel.equals(CF_UNKNOWN) && !carrierLabel.equals(CF_UNSUPPORTED)) {
|
||||
// Save the supported GNSS or SBAS CF
|
||||
if (s.getGnssType() != GnssType.UNKNOWN) {
|
||||
if (s.getGnssType() != GnssType.SBAS) {
|
||||
supportedGnssCfs.add(carrierLabel);
|
||||
} else {
|
||||
if (s.getSbasType() != SbasType.UNKNOWN) {
|
||||
supportedSbasCfs.add(carrierLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if this is a non-primary carrier frequency
|
||||
if (!CarrierFreqUtils.isPrimaryCarrier(carrierLabel)) {
|
||||
mIsNonPrimaryCarrierFreqInView = true;
|
||||
if (s.getUsedInFix()) {
|
||||
mIsNonPrimaryCarrierFreqInUse = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, SatelliteStatus> satStatuses;
|
||||
if (!satellites.containsKey(key)) {
|
||||
// Create new satellite and add signal
|
||||
satStatuses = new HashMap<>();
|
||||
satStatuses.put(carrierLabel, s);
|
||||
Satellite sat = new Satellite(key, satStatuses);
|
||||
satellites.put(key, sat);
|
||||
if (s.getUsedInFix()) {
|
||||
numSatsUsed++;
|
||||
}
|
||||
if (s.getCn0DbHz() != NO_DATA) {
|
||||
numSatsInView++;
|
||||
}
|
||||
} else {
|
||||
// Add signal to existing satellite
|
||||
Satellite sat = satellites.get(key);
|
||||
satStatuses = sat.getStatus();
|
||||
if (!satStatuses.containsKey(carrierLabel)) {
|
||||
// We found another frequency for this satellite
|
||||
satStatuses.put(carrierLabel, s);
|
||||
int frequenciesInUse = 0;
|
||||
int frequenciesInView = 0;
|
||||
for (SatelliteStatus satelliteStatus : satStatuses.values()) {
|
||||
if (satelliteStatus.getUsedInFix()) {
|
||||
frequenciesInUse++;
|
||||
}
|
||||
if (satelliteStatus.getCn0DbHz() != NO_DATA) {
|
||||
frequenciesInView++;
|
||||
}
|
||||
}
|
||||
if (frequenciesInUse > 1) {
|
||||
mIsDualFrequencyPerSatInUse = true;
|
||||
}
|
||||
if (frequenciesInUse == 1 && s.getUsedInFix()) {
|
||||
// The new frequency we just added was the first in use for this satellite
|
||||
numSatsUsed++;
|
||||
}
|
||||
if (frequenciesInView > 1) {
|
||||
mIsDualFrequencyPerSatInView = true;
|
||||
}
|
||||
if (frequenciesInView == 1 && s.getCn0DbHz() != NO_DATA) {
|
||||
// The new frequency we just added was the first in view for this satellite
|
||||
numSatsInView++;
|
||||
}
|
||||
} else {
|
||||
// This shouldn't happen - we found a satellite signal with the same constellation, sat ID, and carrier frequency (including multiple "unknown" or "unsupported" frequencies) as an existing one
|
||||
mDuplicateCarrierStatuses.put(SatelliteUtils.createGnssStatusKey(s), s);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ConstellationFamily(satellites, new SatelliteMetadata(numSignalsInView, numSignalsUsed, allStatuses.size(), numSatsInView, numSatsUsed, satellites.size()));
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
mGnssSatellites.setValue(null);
|
||||
mSbasSatellites.setValue(null);
|
||||
mSatelliteMetadata.setValue(null);
|
||||
mDuplicateCarrierStatuses = new HashMap<>();
|
||||
mUnknownCarrierStatuses = new HashMap<>();
|
||||
supportedGnss = new HashSet<>();
|
||||
supportedSbas = new HashSet<>();
|
||||
supportedGnssCfs = new HashSet<>();
|
||||
supportedSbasCfs = new HashSet<>();
|
||||
mIsDualFrequencyPerSatInView = false;
|
||||
mIsDualFrequencyPerSatInUse = false;
|
||||
mIsNonPrimaryCarrierFreqInView = false;
|
||||
mIsNonPrimaryCarrierFreqInUse = false;
|
||||
gotFirstFix = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the lifecycle of the observer is ended
|
||||
*/
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
super.onCleared();
|
||||
reset();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,681 @@
|
||||
/*
|
||||
* Copyright 2019-2021 Google LLC, 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
|
||||
*
|
||||
* https://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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Configuration
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationCompat.PRIORITY_LOW
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.android.gpstest.Application.Companion.app
|
||||
import com.android.gpstest.Application.Companion.prefs
|
||||
import com.android.gpstest.io.CsvFileLogger
|
||||
import com.android.gpstest.io.JsonFileLogger
|
||||
import com.android.gpstest.library.data.LocationRepository
|
||||
import com.android.gpstest.library.model.SatelliteGroup
|
||||
import com.android.gpstest.library.model.SatelliteMetadata
|
||||
import com.android.gpstest.library.util.FormatUtils.toNotificationTitle
|
||||
import com.android.gpstest.library.util.IOUtils.deleteOldFiles
|
||||
import com.android.gpstest.library.util.IOUtils.forcePsdsInjection
|
||||
import com.android.gpstest.library.util.IOUtils.forceTimeInjection
|
||||
import com.android.gpstest.library.util.IOUtils.writeMeasurementToLogcat
|
||||
import com.android.gpstest.library.util.IOUtils.writeNavMessageToAndroidStudio
|
||||
import com.android.gpstest.library.util.IOUtils.writeNmeaToAndroidStudio
|
||||
import com.android.gpstest.library.util.LibUIUtils.toNotificationSummary
|
||||
import com.android.gpstest.library.util.PreferenceUtil
|
||||
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.ui.MainActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Service tracks location, logs to files, and shows a notification to the user.
|
||||
*
|
||||
* Flows are kept active by this Service while it is bound and started.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class ForegroundOnlyLocationService : LifecycleService() {
|
||||
/*
|
||||
* Checks whether the bound activity has really gone away (foreground service with notification
|
||||
* created) or simply orientation change (no-op).
|
||||
*/
|
||||
private var configurationChange = false
|
||||
|
||||
private var serviceRunningInForeground = false
|
||||
|
||||
private val localBinder = LocalBinder()
|
||||
private var isBound = false
|
||||
private var isStarted = false
|
||||
private var isForeground = false
|
||||
private lateinit var notificationManager: NotificationManager
|
||||
|
||||
// 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())
|
||||
|
||||
// Repository of location data that the service will observe, injected via Hilt
|
||||
@Inject
|
||||
lateinit var repository: LocationRepository
|
||||
|
||||
// Get a reference to the Job from the Flow so we can stop it from UI events
|
||||
private var locationFlow: Job? = null
|
||||
private var nmeaFlow: Job? = null
|
||||
private var navMessageFlow: Job? = null
|
||||
private var measurementFlow: Job? = null
|
||||
private var antennaFlow: Job? = null
|
||||
private var gnssFlow: Job? = null
|
||||
private var sensorFlow: Job? = null
|
||||
|
||||
lateinit var csvFileLogger: CsvFileLogger
|
||||
lateinit var jsonFileLogger: JsonFileLogger
|
||||
|
||||
// Preference listener that will init the loggers if the user changes Settings while Service is running
|
||||
private val loggingSettingListener: SharedPreferences.OnSharedPreferenceChangeListener =
|
||||
PreferenceUtil.newFileLoggingListener(app, { initLogging() }, prefs)
|
||||
private var deletedFiles = false
|
||||
private var injectedAssistData = false
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "onCreate()")
|
||||
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
csvFileLogger = CsvFileLogger(applicationContext)
|
||||
jsonFileLogger = JsonFileLogger(applicationContext)
|
||||
|
||||
// Observe logging setting changes
|
||||
Application.prefs.registerOnSharedPreferenceChangeListener(loggingSettingListener)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Log.d(TAG, "onStartCommand()")
|
||||
|
||||
val cancelLocationTrackingFromNotification =
|
||||
intent?.getBooleanExtra(EXTRA_CANCEL_LOCATION_TRACKING_FROM_NOTIFICATION, false)
|
||||
|
||||
if (cancelLocationTrackingFromNotification == true) {
|
||||
unsubscribeToLocationUpdates()
|
||||
} else {
|
||||
if (!isStarted) {
|
||||
isStarted = true
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
initLogging()
|
||||
}
|
||||
try {
|
||||
observeFlows()
|
||||
} catch (unlikely: Exception) {
|
||||
PreferenceUtils.saveTrackingStarted(false, prefs)
|
||||
Log.e(TAG, "Exception registering for updates: $unlikely")
|
||||
}
|
||||
|
||||
// We may have been restarted by the system. Manage our lifetime accordingly.
|
||||
goForegroundOrStopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
// Tells the system to recreate the service after it's been killed.
|
||||
return super.onStartCommand(intent, flags, START_NOT_STICKY)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
super.onBind(intent)
|
||||
Log.d(TAG, "onBind()")
|
||||
configurationChange = false
|
||||
handleBind()
|
||||
return localBinder
|
||||
}
|
||||
|
||||
override fun onRebind(intent: Intent) {
|
||||
Log.d(TAG, "onRebind()")
|
||||
|
||||
// MainActivity (client) returns to the foreground and rebinds to service, so the service
|
||||
// can become a background services.
|
||||
//stopForeground(true)
|
||||
//serviceRunningInForeground = false
|
||||
configurationChange = false
|
||||
super.onRebind(intent)
|
||||
handleBind()
|
||||
}
|
||||
|
||||
private fun handleBind() {
|
||||
if (!isBound) {
|
||||
isBound = true
|
||||
// Start ourself. This will begin collecting exercise state if we aren't already.
|
||||
//startService(Intent(this, this::class.java))
|
||||
// As long as a UI client is bound to us, we can hide the ongoing activity notification.
|
||||
//removeOngoingActivityNotification()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onUnbind(intent: Intent): Boolean {
|
||||
isBound = false
|
||||
lifecycleScope.launch {
|
||||
// Client can unbind because it went through a configuration change, in which case it
|
||||
// will be recreated and bind again shortly. Wait a few seconds, and if still not bound,
|
||||
// manage our lifetime accordingly.
|
||||
delay(UNBIND_DELAY_MILLIS)
|
||||
if (!isBound) {
|
||||
goForegroundOrStopSelf()
|
||||
}
|
||||
}
|
||||
// Allow clients to re-bind. We will be informed of this in onRebind().
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d(TAG, "onDestroy()")
|
||||
stopLogging()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
configurationChange = true
|
||||
}
|
||||
|
||||
fun subscribeToLocationUpdates() {
|
||||
Log.d(TAG, "subscribeToLocationUpdates()")
|
||||
|
||||
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
|
||||
// be officially started (which we do here).
|
||||
startService(Intent(applicationContext, ForegroundOnlyLocationService::class.java))
|
||||
}
|
||||
|
||||
fun unsubscribeToLocationUpdates() {
|
||||
Log.d(TAG, "unsubscribeToLocationUpdates()")
|
||||
|
||||
try {
|
||||
cancelFlows()
|
||||
stopSelf()
|
||||
stopLogging()
|
||||
isStarted = false
|
||||
PreferenceUtils.saveTrackingStarted(false, prefs)
|
||||
removeOngoingActivityNotification()
|
||||
currentLocation = null
|
||||
currentSatellites = SatelliteGroup(emptyMap(), SatelliteMetadata())
|
||||
} catch (unlikely: SecurityException) {
|
||||
PreferenceUtils.saveTrackingStarted(true, prefs)
|
||||
Log.e(TAG, "Lost location permissions. Couldn't remove updates. $unlikely")
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeFlows() {
|
||||
observeLocationFlow()
|
||||
observeGnssFlow()
|
||||
observeNmeaFlow()
|
||||
observeNavMessageFlow()
|
||||
observeMeasurementsFlow()
|
||||
observeSensorFlow()
|
||||
if (SatelliteUtils.isGnssAntennaInfoSupported(getSystemService(Context.LOCATION_SERVICE) as LocationManager)) {
|
||||
observeAntennaFlow()
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelFlows() {
|
||||
locationFlow?.cancel()
|
||||
gnssFlow?.cancel()
|
||||
nmeaFlow?.cancel()
|
||||
navMessageFlow?.cancel()
|
||||
measurementFlow?.cancel()
|
||||
antennaFlow?.cancel()
|
||||
sensorFlow?.cancel()
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeLocationFlow() {
|
||||
if (locationFlow?.isActive == true) {
|
||||
// If we're already observing updates, don't register again
|
||||
return
|
||||
}
|
||||
// Observe via Flow as they are generated by the repository
|
||||
locationFlow = repository.getLocations()
|
||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
||||
.onEach {
|
||||
//Log.d(TAG, "Service location: ${it.toNotificationTitle()}")
|
||||
currentLocation = it
|
||||
|
||||
// Show location in notification
|
||||
notificationManager.notify(
|
||||
NOTIFICATION_ID,
|
||||
buildNotification(it, currentSatellites)
|
||||
)
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
if (writeLocationToFile(app, prefs)) {
|
||||
initLogging()
|
||||
csvFileLogger.onLocationChanged(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeGnssFlow() {
|
||||
if (gnssFlow?.isActive == true) {
|
||||
// If we're already observing updates, don't register again
|
||||
return
|
||||
}
|
||||
// Observe locations via Flow as they are generated by the repository
|
||||
gnssFlow = repository.getGnssStatus()
|
||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
||||
.map { it.toSatelliteStatus() }
|
||||
.onEach {
|
||||
//Log.d(TAG, "Service SatelliteStatus: $it")
|
||||
// Note - this Flow needs to be active so the Activity/Fragments get TTFF
|
||||
// when it's created while the service is running in the background
|
||||
currentSatellites = it.toSatelliteGroup()
|
||||
|
||||
// Show location in notification
|
||||
notificationManager.notify(
|
||||
NOTIFICATION_ID,
|
||||
buildNotification(currentLocation, currentSatellites)
|
||||
)
|
||||
// Log Status
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
if (writeStatusToFile(app, prefs)) {
|
||||
initLogging()
|
||||
csvFileLogger.onGnssStatusChanged(it, currentLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeNmeaFlow() {
|
||||
if (nmeaFlow?.isActive == true) {
|
||||
// If we're already observing updates, don't register again
|
||||
return
|
||||
}
|
||||
// Observe via Flow as they are generated by the repository
|
||||
nmeaFlow = repository.getNmea()
|
||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
||||
.onEach {
|
||||
//Log.d(TAG, "Service NMEA: $it")
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
if (writeNmeaToAndroidMonitor(app, prefs)) {
|
||||
writeNmeaToAndroidStudio(
|
||||
it.message,
|
||||
if (writeNmeaTimestampToLogcat(app, prefs)) it.timestamp else Long.MIN_VALUE
|
||||
)
|
||||
}
|
||||
if (writeNmeaToFile(app, prefs)) {
|
||||
initLogging()
|
||||
csvFileLogger.onNmeaReceived(it.timestamp, it.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeNavMessageFlow() {
|
||||
if (navMessageFlow?.isActive == true) {
|
||||
// If we're already observing updates, don't register again
|
||||
return
|
||||
}
|
||||
// Observe via Flow as they are generated by the repository
|
||||
navMessageFlow = repository.getNavMessages()
|
||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
||||
.onEach {
|
||||
//Log.d(TAG, "Service nav message: $it")
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
if (writeNavMessageToLogcat(app, prefs)) {
|
||||
writeNavMessageToAndroidStudio(it)
|
||||
}
|
||||
if (writeNavMessageToFile(app, prefs)) {
|
||||
initLogging()
|
||||
csvFileLogger.onGnssNavigationMessageReceived(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeMeasurementsFlow() {
|
||||
if (measurementFlow?.isActive == true) {
|
||||
// If we're already observing updates, don't register again
|
||||
return
|
||||
}
|
||||
// Observe via Flow as they are generated by the repository
|
||||
measurementFlow = repository.getMeasurements()
|
||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
||||
.onEach {
|
||||
//Log.d(TAG, "Service measurement: $it")
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
if (writeMeasurementToLogcat(app, prefs)) {
|
||||
for (m in it.measurements) {
|
||||
writeMeasurementToLogcat(m)
|
||||
}
|
||||
}
|
||||
if (writeMeasurementsToFile(app, prefs)) {
|
||||
initLogging()
|
||||
csvFileLogger.onGnssMeasurementsReceived(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeAntennaFlow() {
|
||||
if (antennaFlow?.isActive == true) {
|
||||
// If we're already observing updates, don't register again
|
||||
return
|
||||
}
|
||||
// Observe via Flow as they are generated by the repository
|
||||
antennaFlow = repository.getAntennas()
|
||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
||||
.onEach {
|
||||
//Log.d(TAG, "Service antennas: $it")
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
if (writeAntennaInfoToFileCsv(app, prefs) || writeAntennaInfoToFileJson(app, prefs)) {
|
||||
initLogging()
|
||||
}
|
||||
if (writeAntennaInfoToFileCsv(app, prefs)) {
|
||||
csvFileLogger.onGnssAntennaInfoReceived(it)
|
||||
}
|
||||
if (writeAntennaInfoToFileJson(app, prefs)) {
|
||||
jsonFileLogger.onGnssAntennaInfoReceived(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeSensorFlow() {
|
||||
if (sensorFlow?.isActive == true) {
|
||||
// If we're already observing updates, don't register again
|
||||
return
|
||||
}
|
||||
// Observe locations via Flow as they are generated by the repository
|
||||
sensorFlow = repository.getSensorUpdates()
|
||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
||||
.onEach {
|
||||
//Log.d(TAG, "Service sensor: orientation ${it.values[0]}, tilt ${it.values[1]}")
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
if (writeOrientationToFile(app, prefs)) {
|
||||
initLogging()
|
||||
csvFileLogger.onOrientationChanged(
|
||||
it,
|
||||
System.currentTimeMillis(),
|
||||
SystemClock.elapsedRealtime()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private fun goForegroundOrStopSelf() {
|
||||
lifecycleScope.launch {
|
||||
// We may have been restarted by the system - check if we're still monitoring data
|
||||
if (PreferenceUtils.isTrackingStarted(prefs)) {
|
||||
// Monitoring GNSS data
|
||||
postOngoingActivityNotification()
|
||||
} else {
|
||||
// We have nothing to do, so we can stop.
|
||||
stopSelf()
|
||||
isStarted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun postOngoingActivityNotification() {
|
||||
if (!isForeground) {
|
||||
isForeground = true
|
||||
Log.d(TAG, "Posting ongoing activity notification")
|
||||
|
||||
createNotificationChannel()
|
||||
startForeground(NOTIFICATION_ID, buildNotification(currentLocation, currentSatellites))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val notificationChannel = NotificationChannel(
|
||||
NOTIFICATION_CHANNEL,
|
||||
getString(R.string.app_name),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
|
||||
// Adds NotificationChannel to system. Attempting to create an
|
||||
// existing notification channel with its original values performs
|
||||
// no operation, so it's safe to perform the below sequence.
|
||||
notificationManager.createNotificationChannel(notificationChannel)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Generates a BIG_TEXT_STYLE Notification that represent latest location.
|
||||
*/
|
||||
private fun buildNotification(location: Location?, satellites: SatelliteGroup): Notification {
|
||||
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()
|
||||
.bigText(summaryText)
|
||||
.setBigContentTitle(titleText)
|
||||
|
||||
// 3. Set up main Intent/Pending Intents for notification
|
||||
val launchActivityIntent = Intent(this, MainActivity::class.java).apply {
|
||||
flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK
|
||||
// NOTE: The above causes the activity/viewmodel to be recreated from scratch for Accuracy when it's already visible
|
||||
// and the notification is tapped (strangely if it's destroyed Accuracy viewmodel seems to keep it's state)
|
||||
// FLAG_ACTIVITY_REORDER_TO_FRONT seems like it should work, but if this is used then onResume() is called
|
||||
// again (and onPause() is never called). This seems to freeze up Status into a blank state because GNSS inits again.
|
||||
}
|
||||
val openActivityPendingIntent = PendingIntentCompat.getActivity(
|
||||
applicationContext,
|
||||
System.currentTimeMillis().toInt(),
|
||||
launchActivityIntent,
|
||||
0,
|
||||
false
|
||||
)
|
||||
|
||||
val cancelIntent = Intent(this, ForegroundOnlyLocationService::class.java).apply {
|
||||
putExtra(EXTRA_CANCEL_LOCATION_TRACKING_FROM_NOTIFICATION, true)
|
||||
}
|
||||
val stopServicePendingIntent = PendingIntentCompat.getService(
|
||||
applicationContext,
|
||||
System.currentTimeMillis().toInt(),
|
||||
cancelIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
false
|
||||
)
|
||||
|
||||
// 4. Build and issue the notification.
|
||||
// Notification Channel Id is ignored for Android pre O (26).
|
||||
val notificationCompatBuilder =
|
||||
NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL)
|
||||
|
||||
return notificationCompatBuilder
|
||||
.setStyle(bigTextStyle)
|
||||
.setContentTitle(titleText)
|
||||
.setContentText(summaryText)
|
||||
.setSmallIcon(R.drawable.ic_sat_notification)
|
||||
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
||||
.setOngoing(true)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setPriority(PRIORITY_LOW) // For < API 26
|
||||
.setDefaults(NotificationCompat.DEFAULT_LIGHTS) // For < API 26
|
||||
.setContentIntent(openActivityPendingIntent)
|
||||
.addAction(
|
||||
R.drawable.ic_baseline_launch_24, getString(R.string.open),
|
||||
openActivityPendingIntent
|
||||
)
|
||||
.addAction(
|
||||
R.drawable.ic_baseline_cancel_24,
|
||||
getString(R.string.stop),
|
||||
stopServicePendingIntent
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun removeOngoingActivityNotification() {
|
||||
if (isForeground) {
|
||||
Log.d(TAG, "Removing ongoing activity notification")
|
||||
isForeground = false
|
||||
stopForeground(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize and start logging if permissions have been granted.
|
||||
*
|
||||
* Note that this is called from each of the flows that log data, because when the user initially
|
||||
* enables logging in the settings the preference change callback happens before the user grants
|
||||
* file permissions. So we need to call this on each update in case the user just granted file
|
||||
* permissions but logging hasn't been started yet.
|
||||
*/
|
||||
@Synchronized
|
||||
private fun initLogging() {
|
||||
// Inject time and/or PSDS to make sure timestamps and assistance are as updated as possible
|
||||
maybeInjectAssistData()
|
||||
|
||||
val date = Date()
|
||||
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(app, prefs)) {
|
||||
jsonFileLogger.startLog(null, date)
|
||||
}
|
||||
maybeDeleteFiles()
|
||||
}
|
||||
|
||||
private fun maybeInjectAssistData() {
|
||||
if (injectedAssistData) {
|
||||
// Only inject once per logging session
|
||||
return
|
||||
}
|
||||
val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
if (PreferenceUtil.injectTimeWhenLogging(app, prefs)) {
|
||||
forceTimeInjection(app, locationManager)
|
||||
}
|
||||
if (PreferenceUtil.injectPsdsWhenLogging(app, prefs)) {
|
||||
forcePsdsInjection(app, locationManager)
|
||||
}
|
||||
injectedAssistData = true
|
||||
}
|
||||
|
||||
private fun maybeDeleteFiles() {
|
||||
if (deletedFiles) {
|
||||
// If we've already deleted files on this application execution, don't do it again
|
||||
return
|
||||
}
|
||||
if (csvFileLogger.isStarted || jsonFileLogger.isStarted) {
|
||||
// Base directories should be the same, so we only need one of the two (whichever is logging) to clear old files
|
||||
var baseDirectory: File = csvFileLogger.baseDirectory
|
||||
if (baseDirectory == null) {
|
||||
baseDirectory = jsonFileLogger.baseDirectory
|
||||
}
|
||||
deleteOldFiles(baseDirectory, csvFileLogger.file, jsonFileLogger.file)
|
||||
deletedFiles = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopLogging() {
|
||||
csvFileLogger.close()
|
||||
jsonFileLogger.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Class used for the client Binder. Since this service runs in the same process as its
|
||||
* clients, we don't need to deal with IPC.
|
||||
*/
|
||||
inner class LocalBinder : Binder() {
|
||||
val service: ForegroundOnlyLocationService
|
||||
get() = this@ForegroundOnlyLocationService
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "LocationService"
|
||||
|
||||
private const val PACKAGE_NAME = "com.android.gpstest"
|
||||
|
||||
private const val EXTRA_CANCEL_LOCATION_TRACKING_FROM_NOTIFICATION =
|
||||
"$PACKAGE_NAME.extra.CANCEL_LOCATION_TRACKING_FROM_NOTIFICATION"
|
||||
|
||||
private const val NOTIFICATION_ID = 12345678
|
||||
|
||||
private const val NOTIFICATION_CHANNEL = "gsptest_channel_01"
|
||||
|
||||
private const val UNBIND_DELAY_MILLIS = 3_000L
|
||||
}
|
||||
}
|
||||
@@ -1,551 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2008-2013 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.
|
||||
*/
|
||||
|
||||
package com.android.gpstest;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.location.GnssMeasurementsEvent;
|
||||
import android.location.GnssStatus;
|
||||
import android.location.GpsStatus;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.Transformation;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
|
||||
import com.android.gpstest.util.MathUtils;
|
||||
import com.android.gpstest.util.UIUtils;
|
||||
import com.android.gpstest.view.GpsSkyView;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class GpsSkyFragment extends Fragment implements GpsTestListener {
|
||||
|
||||
public final static String TAG = "GpsSkyFragment";
|
||||
|
||||
private GpsSkyView mSkyView;
|
||||
|
||||
private List<View> mLegendLines;
|
||||
|
||||
private List<ImageView> mLegendShapes;
|
||||
|
||||
private TextView mLegendCn0Title, mLegendCn0Units, mLegendCn0LeftText, mLegendCn0LeftCenterText,
|
||||
mLegendCn0CenterText, mLegendCn0RightCenterText, mLegendCn0RightText, mSnrCn0InViewAvgText, mSnrCn0UsedAvgText;
|
||||
|
||||
private ImageView mSnrCn0InViewAvg, mSnrCn0UsedAvg, lock, circleUsedInFix;
|
||||
|
||||
Animation mSnrCn0InViewAvgAnimation, mSnrCn0UsedAvgAnimation, mSnrCn0InViewAvgAnimationTextView, mSnrCn0UsedAvgAnimationTextView;
|
||||
|
||||
private boolean mUseLegacyGnssApi = false;
|
||||
|
||||
// Default light theme values
|
||||
int usedCn0Background = R.drawable.cn0_round_corner_background_used;
|
||||
int usedCn0IndicatorColor = Color.BLACK;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View v = inflater.inflate(R.layout.gps_sky, container,false);
|
||||
|
||||
mSkyView = v.findViewById(R.id.sky_view);
|
||||
|
||||
initLegendViews(v);
|
||||
|
||||
mSnrCn0InViewAvg = v.findViewById(R.id.cn0_indicator_in_view);
|
||||
mSnrCn0UsedAvg = v.findViewById(R.id.cn0_indicator_used);
|
||||
lock = v.findViewById(R.id.sky_lock);
|
||||
circleUsedInFix = v.findViewById(R.id.sky_legend_used_in_fix);
|
||||
|
||||
GpsTestActivity.getInstance().addListener(this);
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
int color;
|
||||
if (Application.getPrefs().getBoolean(getString(R.string.pref_key_dark_theme), false)) {
|
||||
// Dark theme
|
||||
color = getResources().getColor(android.R.color.secondary_text_dark);
|
||||
circleUsedInFix.setImageResource(R.drawable.circle_used_in_fix_dark);
|
||||
usedCn0Background = R.drawable.cn0_round_corner_background_used_dark;
|
||||
usedCn0IndicatorColor = getResources().getColor(android.R.color.darker_gray);
|
||||
} else {
|
||||
// Light theme
|
||||
color = getResources().getColor(R.color.body_text_2_light);
|
||||
circleUsedInFix.setImageResource(R.drawable.circle_used_in_fix);
|
||||
usedCn0Background = R.drawable.cn0_round_corner_background_used;
|
||||
usedCn0IndicatorColor = Color.BLACK;
|
||||
}
|
||||
for (View v : mLegendLines) {
|
||||
v.setBackgroundColor(color);
|
||||
}
|
||||
for (ImageView v : mLegendShapes) {
|
||||
v.setColorFilter(color);
|
||||
}
|
||||
}
|
||||
|
||||
public void onLocationChanged(Location loc) {
|
||||
}
|
||||
|
||||
public void onStatusChanged(String provider, int status, Bundle extras) {
|
||||
}
|
||||
|
||||
public void onProviderEnabled(String provider) {
|
||||
}
|
||||
|
||||
public void onProviderDisabled(String provider) {
|
||||
}
|
||||
|
||||
public void gpsStart() {
|
||||
}
|
||||
|
||||
public void gpsStop() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssFirstFix(int ttffMillis) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssFixAcquired() {
|
||||
showHaveFix();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssFixLost() {
|
||||
showLostFix();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@Override
|
||||
public void onSatelliteStatusChanged(GnssStatus status) {
|
||||
mSkyView.setGnssStatus(status);
|
||||
mUseLegacyGnssApi = false;
|
||||
updateSnrCn0AvgMeterText();
|
||||
updateSnrCn0Avgs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssStarted() {
|
||||
mSkyView.setStarted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssStopped() {
|
||||
mSkyView.setStopped();
|
||||
if (lock != null) {
|
||||
lock.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@Override
|
||||
public void onGnssMeasurementsReceived(GnssMeasurementsEvent event) {
|
||||
mSkyView.setGnssMeasurementEvent(event);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void onGpsStatusChanged(int event, GpsStatus status) {
|
||||
switch (event) {
|
||||
case GpsStatus.GPS_EVENT_STARTED:
|
||||
mSkyView.setStarted();
|
||||
break;
|
||||
|
||||
case GpsStatus.GPS_EVENT_STOPPED:
|
||||
mSkyView.setStopped();
|
||||
break;
|
||||
|
||||
case GpsStatus.GPS_EVENT_SATELLITE_STATUS:
|
||||
mSkyView.setSats(status);
|
||||
mUseLegacyGnssApi = true;
|
||||
updateSnrCn0AvgMeterText();
|
||||
updateSnrCn0Avgs();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOrientationChanged(double orientation, double tilt) {
|
||||
// For performance reasons, only proceed if this fragment is visible
|
||||
if (!getUserVisibleHint()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mSkyView != null) {
|
||||
mSkyView.onOrientationChanged(orientation, tilt);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNmeaMessage(String message, long timestamp) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the views in the C/N0 and Shape legends
|
||||
* @param v view in which the legend view IDs can be found via view.findViewById()
|
||||
*/
|
||||
private void initLegendViews(View v) {
|
||||
if (mLegendLines == null) {
|
||||
mLegendLines = new LinkedList<>();
|
||||
} else {
|
||||
mLegendLines.clear();
|
||||
}
|
||||
|
||||
if (mLegendShapes == null) {
|
||||
mLegendShapes = new LinkedList<>();
|
||||
} else {
|
||||
mLegendShapes.clear();
|
||||
}
|
||||
|
||||
// Avg C/N0 indicator lines
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_cn0_left_line4));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_cn0_left_line3));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_cn0_left_line2));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_cn0_left_line1));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_cn0_center_line));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_cn0_right_line1));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_cn0_right_line2));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_cn0_right_line3));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_cn0_right_line4));
|
||||
|
||||
// Shape Legend lines
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line1a));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line1b));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line2a));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line2b));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line3a));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line3b));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line4a));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line4b));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line5a));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line5b));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line6a));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line6b));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line7a));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line7b));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line8a));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line8b));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line9a));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line9b));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line10a));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line10b));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line11a));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line12a));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line13a));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line14a));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line14b));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line15a));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line15b));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line16a));
|
||||
mLegendLines.add(v.findViewById(R.id.sky_legend_shape_line16b));
|
||||
|
||||
// Shape Legend shapes
|
||||
mLegendShapes.add((ImageView) v.findViewById(R.id.sky_legend_circle));
|
||||
mLegendShapes.add((ImageView) v.findViewById(R.id.sky_legend_square));
|
||||
mLegendShapes.add((ImageView) v.findViewById(R.id.sky_legend_pentagon));
|
||||
mLegendShapes.add((ImageView) v.findViewById(R.id.sky_legend_triangle));
|
||||
mLegendShapes.add((ImageView) v.findViewById(R.id.sky_legend_hexagon1));
|
||||
mLegendShapes.add((ImageView) v.findViewById(R.id.sky_legend_oval));
|
||||
mLegendShapes.add((ImageView) v.findViewById(R.id.sky_legend_diamond1));
|
||||
mLegendShapes.add((ImageView) v.findViewById(R.id.sky_legend_diamond2));
|
||||
mLegendShapes.add((ImageView) v.findViewById(R.id.sky_legend_diamond3));
|
||||
mLegendShapes.add((ImageView) v.findViewById(R.id.sky_legend_diamond4));
|
||||
mLegendShapes.add((ImageView) v.findViewById(R.id.sky_legend_diamond5));
|
||||
mLegendShapes.add((ImageView) v.findViewById(R.id.sky_legend_diamond6));
|
||||
mLegendShapes.add((ImageView) v.findViewById(R.id.sky_legend_diamond7));
|
||||
|
||||
// C/N0 Legend text
|
||||
mLegendCn0Title = v.findViewById(R.id.sky_legend_cn0_title);
|
||||
mLegendCn0Units = v.findViewById(R.id.sky_legend_cn0_units);
|
||||
mLegendCn0LeftText = v.findViewById(R.id.sky_legend_cn0_left_text);
|
||||
mLegendCn0LeftCenterText = v.findViewById(R.id.sky_legend_cn0_left_center_text);
|
||||
mLegendCn0CenterText = v.findViewById(R.id.sky_legend_cn0_center_text);
|
||||
mLegendCn0RightCenterText = v.findViewById(R.id.sky_legend_cn0_right_center_text);
|
||||
mLegendCn0RightText = v.findViewById(R.id.sky_legend_cn0_right_text);
|
||||
mSnrCn0InViewAvgText = v.findViewById(R.id.cn0_text_in_view);
|
||||
mSnrCn0UsedAvgText = v.findViewById(R.id.cn0_text_used);
|
||||
}
|
||||
|
||||
private void updateSnrCn0AvgMeterText() {
|
||||
if (!mUseLegacyGnssApi || (mSkyView != null && mSkyView.isSnrBad())) {
|
||||
// C/N0
|
||||
mLegendCn0Title.setText(R.string.gps_cn0_column_label);
|
||||
mLegendCn0Units.setText(R.string.sky_legend_cn0_units);
|
||||
mLegendCn0LeftText.setText(R.string.sky_legend_cn0_low);
|
||||
mLegendCn0LeftCenterText.setText(R.string.sky_legend_cn0_low_middle);
|
||||
mLegendCn0CenterText.setText(R.string.sky_legend_cn0_middle);
|
||||
mLegendCn0RightCenterText.setText(R.string.sky_legend_cn0_middle_high);
|
||||
mLegendCn0RightText.setText(R.string.sky_legend_cn0_high);
|
||||
} else {
|
||||
// SNR for Android 6.0 and lower (or if user unchecked "Use GNSS APIs" setting and values conform to SNR range)
|
||||
mLegendCn0Title.setText(R.string.gps_snr_column_label);
|
||||
mLegendCn0Units.setText(R.string.sky_legend_snr_units);
|
||||
mLegendCn0LeftText.setText(R.string.sky_legend_snr_low);
|
||||
mLegendCn0LeftCenterText.setText(R.string.sky_legend_snr_low_middle);
|
||||
mLegendCn0CenterText.setText(R.string.sky_legend_snr_middle);
|
||||
mLegendCn0RightCenterText.setText(R.string.sky_legend_snr_middle_high);
|
||||
mLegendCn0RightText.setText(R.string.sky_legend_snr_high);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSnrCn0Avgs() {
|
||||
if (mSkyView == null) {
|
||||
return;
|
||||
}
|
||||
// Based on the avg SNR or C/N0 for "in view" and "used" satellites the left margins need to be adjusted accordingly
|
||||
int meterWidthPx = (int) Application.get().getResources().getDimension(R.dimen.cn0_meter_width)
|
||||
- UIUtils.dpToPixels(Application.get(), 7.0f); // Reduce width for padding
|
||||
int minIndicatorMarginPx = (int) Application.get().getResources().getDimension(R.dimen.cn0_indicator_min_left_margin);
|
||||
int maxIndicatorMarginPx = meterWidthPx + minIndicatorMarginPx;
|
||||
int minTextViewMarginPx = (int) Application.get().getResources().getDimension(R.dimen.cn0_textview_min_left_margin);
|
||||
int maxTextViewMarginPx = meterWidthPx + minTextViewMarginPx;
|
||||
|
||||
// When both "in view" and "used" indicators and TextViews are shown, slide the "in view" TextView by this amount to the left to avoid overlap
|
||||
float TEXTVIEW_NON_OVERLAP_OFFSET_DP = -16.0f;
|
||||
|
||||
// Calculate normal offsets for avg in view satellite SNR or C/N0 value TextViews
|
||||
Integer leftInViewTextViewMarginPx = null;
|
||||
if (MathUtils.isValidFloat(mSkyView.getSnrCn0InViewAvg())) {
|
||||
if (!mSkyView.isUsingLegacyGpsApi() || mSkyView.isSnrBad()) {
|
||||
// C/N0
|
||||
leftInViewTextViewMarginPx = UIUtils.cn0ToTextViewLeftMarginPx(mSkyView.getSnrCn0InViewAvg(),
|
||||
minTextViewMarginPx, maxTextViewMarginPx);
|
||||
} else {
|
||||
// SNR
|
||||
leftInViewTextViewMarginPx = UIUtils.snrToTextViewLeftMarginPx(mSkyView.getSnrCn0InViewAvg(),
|
||||
minTextViewMarginPx, maxTextViewMarginPx);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate normal offsets for avg used satellite C/N0 value TextViews
|
||||
Integer leftUsedTextViewMarginPx = null;
|
||||
if (MathUtils.isValidFloat(mSkyView.getSnrCn0UsedAvg())) {
|
||||
if (!mSkyView.isUsingLegacyGpsApi() || mSkyView.isSnrBad()) {
|
||||
// C/N0
|
||||
leftUsedTextViewMarginPx = UIUtils.cn0ToTextViewLeftMarginPx(mSkyView.getSnrCn0UsedAvg(),
|
||||
minTextViewMarginPx, maxTextViewMarginPx);
|
||||
} else {
|
||||
// SNR
|
||||
leftUsedTextViewMarginPx = UIUtils.snrToTextViewLeftMarginPx(mSkyView.getSnrCn0UsedAvg(),
|
||||
minTextViewMarginPx, maxTextViewMarginPx);
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
int offset = UIUtils.dpToPixels(Application.get(), TEXTVIEW_NON_OVERLAP_OFFSET_DP);
|
||||
if (leftInViewTextViewMarginPx <= leftUsedTextViewMarginPx) {
|
||||
leftInViewTextViewMarginPx += offset;
|
||||
} else {
|
||||
leftUsedTextViewMarginPx += offset;
|
||||
}
|
||||
}
|
||||
|
||||
// Define paddings used for TextViews
|
||||
int pSides = UIUtils.dpToPixels(Application.get(), 7);
|
||||
int pTopBottom = UIUtils.dpToPixels(Application.get(), 4);
|
||||
|
||||
// Set avg SNR or C/N0 of satellites in view of device
|
||||
if (MathUtils.isValidFloat(mSkyView.getSnrCn0InViewAvg())) {
|
||||
mSnrCn0InViewAvgText.setText(String.format("%.1f", mSkyView.getSnrCn0InViewAvg()));
|
||||
|
||||
// Set color of TextView
|
||||
int color = mSkyView.getSatelliteColor(mSkyView.getSnrCn0InViewAvg());
|
||||
LayerDrawable background = (LayerDrawable) ContextCompat.getDrawable(Application.get(), R.drawable.cn0_round_corner_background_in_view);
|
||||
|
||||
// Fill
|
||||
GradientDrawable backgroundGradient = (GradientDrawable) background.findDrawableByLayerId(R.id.cn0_avg_in_view_fill);
|
||||
backgroundGradient.setColor(color);
|
||||
|
||||
// Stroke
|
||||
GradientDrawable borderGradient = (GradientDrawable) background.findDrawableByLayerId(R.id.cn0_avg_in_view_border);
|
||||
borderGradient.setColor(color);
|
||||
|
||||
mSnrCn0InViewAvgText.setBackground(background);
|
||||
|
||||
// Set padding
|
||||
mSnrCn0InViewAvgText.setPadding(pSides, pTopBottom, pSides, pTopBottom);
|
||||
|
||||
// Set color of indicator
|
||||
mSnrCn0InViewAvg.setColorFilter(color);
|
||||
|
||||
// Set position and visibility of TextView
|
||||
if (mSnrCn0InViewAvgText.getVisibility() == View.VISIBLE) {
|
||||
animateSnrCn0Indicator(mSnrCn0InViewAvgText, leftInViewTextViewMarginPx, mSnrCn0InViewAvgAnimationTextView);
|
||||
} else {
|
||||
RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) mSnrCn0InViewAvgText.getLayoutParams();
|
||||
lp.setMargins(leftInViewTextViewMarginPx, lp.topMargin, lp.rightMargin, lp.bottomMargin);
|
||||
mSnrCn0InViewAvgText.setLayoutParams(lp);
|
||||
mSnrCn0InViewAvgText.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// Set position and visibility of indicator
|
||||
int leftIndicatorMarginPx;
|
||||
if (!mSkyView.isUsingLegacyGpsApi() || mSkyView.isSnrBad()) {
|
||||
// C/N0
|
||||
leftIndicatorMarginPx = UIUtils.cn0ToIndicatorLeftMarginPx(mSkyView.getSnrCn0InViewAvg(),
|
||||
minIndicatorMarginPx, maxIndicatorMarginPx);
|
||||
} else {
|
||||
// SNR
|
||||
leftIndicatorMarginPx = UIUtils.snrToIndicatorLeftMarginPx(mSkyView.getSnrCn0InViewAvg(),
|
||||
minIndicatorMarginPx, maxIndicatorMarginPx);
|
||||
}
|
||||
|
||||
// If the view is already visible, animate to the new position. Otherwise just set the position and make it visible
|
||||
if (mSnrCn0InViewAvg.getVisibility() == View.VISIBLE) {
|
||||
animateSnrCn0Indicator(mSnrCn0InViewAvg, leftIndicatorMarginPx, mSnrCn0InViewAvgAnimation);
|
||||
} else {
|
||||
RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) mSnrCn0InViewAvg.getLayoutParams();
|
||||
lp.setMargins(leftIndicatorMarginPx, lp.topMargin, lp.rightMargin, lp.bottomMargin);
|
||||
mSnrCn0InViewAvg.setLayoutParams(lp);
|
||||
mSnrCn0InViewAvg.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
mSnrCn0InViewAvgText.setText("");
|
||||
mSnrCn0InViewAvgText.setVisibility(View.INVISIBLE);
|
||||
mSnrCn0InViewAvg.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
// Set avg SNR or C/N0 of satellites used in fix
|
||||
if (MathUtils.isValidFloat(mSkyView.getSnrCn0UsedAvg())) {
|
||||
mSnrCn0UsedAvgText.setText(String.format("%.1f", mSkyView.getSnrCn0UsedAvg()));
|
||||
// Set color of TextView
|
||||
int color = mSkyView.getSatelliteColor(mSkyView.getSnrCn0UsedAvg());
|
||||
LayerDrawable background = (LayerDrawable) ContextCompat.getDrawable(Application.get(), usedCn0Background);
|
||||
|
||||
// Fill
|
||||
GradientDrawable backgroundGradient = (GradientDrawable) background.findDrawableByLayerId(R.id.cn0_avg_used_fill);
|
||||
backgroundGradient.setColor(color);
|
||||
|
||||
mSnrCn0UsedAvgText.setBackground(background);
|
||||
|
||||
// Set padding
|
||||
mSnrCn0UsedAvgText.setPadding(pSides, pTopBottom, pSides, pTopBottom);
|
||||
|
||||
// Set color of indicator
|
||||
mSnrCn0UsedAvg.setColorFilter(usedCn0IndicatorColor);
|
||||
|
||||
// Set position and visibility of TextView
|
||||
if (mSnrCn0UsedAvgText.getVisibility() == View.VISIBLE) {
|
||||
animateSnrCn0Indicator(mSnrCn0UsedAvgText, leftUsedTextViewMarginPx, mSnrCn0UsedAvgAnimationTextView);
|
||||
} else {
|
||||
RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) mSnrCn0UsedAvgText.getLayoutParams();
|
||||
lp.setMargins(leftUsedTextViewMarginPx, lp.topMargin, lp.rightMargin, lp.bottomMargin);
|
||||
mSnrCn0UsedAvgText.setLayoutParams(lp);
|
||||
mSnrCn0UsedAvgText.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// Set position and visibility of indicator
|
||||
int leftMarginPx;
|
||||
if (!mSkyView.isUsingLegacyGpsApi() || mSkyView.isSnrBad()) {
|
||||
// C/N0
|
||||
leftMarginPx = UIUtils.cn0ToIndicatorLeftMarginPx(mSkyView.getSnrCn0UsedAvg(),
|
||||
minIndicatorMarginPx, maxIndicatorMarginPx);
|
||||
} else {
|
||||
// SNR
|
||||
leftMarginPx = UIUtils.snrToIndicatorLeftMarginPx(mSkyView.getSnrCn0UsedAvg(),
|
||||
minIndicatorMarginPx, maxIndicatorMarginPx);
|
||||
}
|
||||
|
||||
// If the view is already visible, animate to the new position. Otherwise just set the position and make it visible
|
||||
if (mSnrCn0UsedAvg.getVisibility() == View.VISIBLE) {
|
||||
animateSnrCn0Indicator(mSnrCn0UsedAvg, leftMarginPx, mSnrCn0UsedAvgAnimation);
|
||||
} else {
|
||||
RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) mSnrCn0UsedAvg.getLayoutParams();
|
||||
lp.setMargins(leftMarginPx, lp.topMargin, lp.rightMargin, lp.bottomMargin);
|
||||
mSnrCn0UsedAvg.setLayoutParams(lp);
|
||||
mSnrCn0UsedAvg.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
mSnrCn0UsedAvgText.setText("");
|
||||
mSnrCn0UsedAvgText.setVisibility(View.INVISIBLE);
|
||||
mSnrCn0UsedAvg.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animates a SNR or C/N0 indicator view from it's current location to the provided left margin location (in pixels)
|
||||
* @param v view to animate
|
||||
* @param goalLeftMarginPx the new left margin for the view that the view should animate to in pixels
|
||||
* @param animation Animation to use for the animation
|
||||
*/
|
||||
private void animateSnrCn0Indicator(final View v, final int goalLeftMarginPx, Animation animation) {
|
||||
if (v == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (animation != null) {
|
||||
animation.reset();
|
||||
}
|
||||
|
||||
final ViewGroup.MarginLayoutParams p = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
|
||||
|
||||
final int currentMargin = p.leftMargin;
|
||||
|
||||
animation = new Animation() {
|
||||
@Override
|
||||
protected void applyTransformation(float interpolatedTime, Transformation t) {
|
||||
int newLeft;
|
||||
if (goalLeftMarginPx > currentMargin) {
|
||||
newLeft = currentMargin + (int) (Math.abs(currentMargin - goalLeftMarginPx)
|
||||
* interpolatedTime);
|
||||
} else {
|
||||
newLeft = currentMargin - (int) (Math.abs(currentMargin - goalLeftMarginPx)
|
||||
* interpolatedTime);
|
||||
}
|
||||
UIUtils.setMargins(v,
|
||||
newLeft,
|
||||
p.topMargin,
|
||||
p.rightMargin,
|
||||
p.bottomMargin);
|
||||
}
|
||||
};
|
||||
// C/N0 updates every second, so animation of 300ms (https://material.io/guidelines/motion/duration-easing.html#duration-easing-common-durations)
|
||||
// wit FastOutSlowInInterpolator recommended by Material Design spec easily finishes in time for next C/N0 update
|
||||
animation.setDuration(300);
|
||||
animation.setInterpolator(new FastOutSlowInInterpolator());
|
||||
v.startAnimation(animation);
|
||||
}
|
||||
|
||||
private void showHaveFix() {
|
||||
if (lock != null) {
|
||||
UIUtils.showViewWithAnimation(lock, UIUtils.ANIMATION_DURATION_SHORT_MS);
|
||||
}
|
||||
}
|
||||
|
||||
private void showLostFix() {
|
||||
if (lock != null) {
|
||||
UIUtils.hideViewWithAnimation(lock, UIUtils.ANIMATION_DURATION_SHORT_MS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package com.android.gpstest;
|
||||
|
||||
import android.location.GnssMeasurementsEvent;
|
||||
import android.location.GnssStatus;
|
||||
import android.location.GpsStatus;
|
||||
import android.location.LocationListener;
|
||||
|
||||
/**
|
||||
* Interface used by GpsTestActivity to communicate with Gps*Fragments
|
||||
*/
|
||||
public interface GpsTestListener extends LocationListener {
|
||||
|
||||
void gpsStart();
|
||||
|
||||
void gpsStop();
|
||||
|
||||
@Deprecated
|
||||
void onGpsStatusChanged(int event, GpsStatus status);
|
||||
|
||||
void onGnssFirstFix(int ttffMillis);
|
||||
|
||||
void onSatelliteStatusChanged(GnssStatus status);
|
||||
|
||||
void onGnssStarted();
|
||||
|
||||
void onGnssStopped();
|
||||
|
||||
void onGnssMeasurementsReceived(GnssMeasurementsEvent event);
|
||||
|
||||
void onOrientationChanged(double orientation, double tilt);
|
||||
|
||||
void onNmeaMessage(String message, long timestamp);
|
||||
|
||||
/**
|
||||
* Called when a GNSS fix is acquired, including on first fix
|
||||
*/
|
||||
void onGnssFixAcquired();
|
||||
|
||||
/**
|
||||
* Called when a GNSS fix is lost, following initial acquisition (this is not called on startup
|
||||
* prior to a fix initially being acquired)
|
||||
*/
|
||||
void onGnssFixLost();
|
||||
}
|
||||
@@ -1,326 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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;
|
||||
|
||||
import static com.android.gpstest.util.SatelliteUtils.isForceFullGnssMeasurementsSupported;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Bundle;
|
||||
import android.preference.CheckBoxPreference;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceActivity;
|
||||
import android.preference.PreferenceCategory;
|
||||
import android.text.InputType;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import com.android.gpstest.util.PermissionUtils;
|
||||
import com.android.gpstest.util.SatelliteUtils;
|
||||
import com.android.gpstest.util.UIUtils;
|
||||
|
||||
public class Preferences extends PreferenceActivity implements
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
CheckBoxPreference forceFullGnssMeasurements;
|
||||
|
||||
EditTextPreference txtMinTime;
|
||||
|
||||
EditTextPreference txtMinDistance;
|
||||
|
||||
CheckBoxPreference chkDarkTheme;
|
||||
|
||||
private Toolbar mActionBar;
|
||||
|
||||
ListPreference preferredDistanceUnits;
|
||||
|
||||
ListPreference preferredSpeedUnits;
|
||||
|
||||
ListPreference language;
|
||||
|
||||
CheckBoxPreference chkLogFileNmea;
|
||||
CheckBoxPreference chkLogFileNavMessages;
|
||||
CheckBoxPreference chkLogFileMeasurements;
|
||||
CheckBoxPreference chkLogFileLocation;
|
||||
CheckBoxPreference chkLogFileAntennaJson;
|
||||
CheckBoxPreference chkLogFileAntennaCsv;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
// Set theme
|
||||
if (Application.getPrefs().getBoolean(getString(R.string.pref_key_dark_theme), false)) {
|
||||
setTheme(R.style.AppTheme_Dark);
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
mActionBar.setTitle(getTitle());
|
||||
|
||||
UIUtils.resetActivityTitle(this);
|
||||
|
||||
forceFullGnssMeasurements = (CheckBoxPreference) this
|
||||
.findPreference(getString(R.string.pref_key_force_full_gnss_measurements));
|
||||
|
||||
if (!isForceFullGnssMeasurementsSupported()) {
|
||||
forceFullGnssMeasurements.setEnabled(false);
|
||||
}
|
||||
|
||||
txtMinTime = (EditTextPreference) this
|
||||
.findPreference(getString(R.string.pref_key_gps_min_time));
|
||||
txtMinTime.getEditText()
|
||||
.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
|
||||
|
||||
// Verify minTime entry
|
||||
txtMinTime.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
if (!verifyFloat(newValue)) {
|
||||
// Tell user that entry must be valid decimal
|
||||
Toast.makeText(
|
||||
Preferences.this,
|
||||
getString(R.string.pref_gps_min_time_invalid_entry),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
txtMinDistance = (EditTextPreference) this
|
||||
.findPreference(getString(R.string.pref_key_gps_min_distance));
|
||||
txtMinDistance.getEditText()
|
||||
.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
|
||||
|
||||
// Verify minDistance entry
|
||||
txtMinDistance.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
if (!verifyFloat(newValue)) {
|
||||
// Tell user that entry must be valid decimal
|
||||
Toast.makeText(
|
||||
Preferences.this,
|
||||
getString(R.string.pref_gps_min_distance_invalid_entry),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Check Dark Theme
|
||||
chkDarkTheme = (CheckBoxPreference) this
|
||||
.findPreference(getString(R.string.pref_key_dark_theme));
|
||||
|
||||
chkDarkTheme.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
// Destroy and recreate Activity
|
||||
recreate();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
preferredDistanceUnits = (ListPreference) findPreference(
|
||||
getString(R.string.pref_key_preferred_distance_units_v2));
|
||||
|
||||
preferredSpeedUnits = (ListPreference) findPreference(
|
||||
getString(R.string.pref_key_preferred_speed_units_v2));
|
||||
|
||||
language = (ListPreference) findPreference(getString(R.string.pref_key_language));
|
||||
language.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
Application.getLocaleManager().setNewLocale(Application.get(), newValue.toString());
|
||||
// Destroy and recreate Activity
|
||||
recreate();
|
||||
return true;
|
||||
});
|
||||
|
||||
// Remove preference for rotating map if needed
|
||||
if (!SatelliteUtils.isRotationVectorSensorSupported(this) || !BuildConfig.FLAVOR.equals("google")) {
|
||||
// We don't have tilt info or it's the OSM Droid flavor, so remove this preference
|
||||
CheckBoxPreference checkBoxTiltMap = (CheckBoxPreference) findPreference(
|
||||
getString(R.string.pref_key_tilt_map_with_sensors));
|
||||
PreferenceCategory mMapCategory = (PreferenceCategory) findPreference(
|
||||
getString(R.string.pref_key_map_category));
|
||||
mMapCategory.removePreference(checkBoxTiltMap);
|
||||
}
|
||||
|
||||
// Remove preference for setting map type if needed
|
||||
if (!BuildConfig.FLAVOR.equals("google")) {
|
||||
// We don't have tilt info or it's the OSM Droid flavor, so remove this preference
|
||||
ListPreference checkBoxMapType = (ListPreference) findPreference(
|
||||
getString(R.string.pref_key_map_type));
|
||||
PreferenceCategory mMapCategory = (PreferenceCategory) findPreference(
|
||||
getString(R.string.pref_key_map_category));
|
||||
mMapCategory.removePreference(checkBoxMapType);
|
||||
}
|
||||
|
||||
// If the user chooses to enable any of the file writing preferences, request permission
|
||||
chkLogFileNmea = (CheckBoxPreference) findPreference(getString(R.string.pref_key_file_nmea_output));
|
||||
chkLogFileNmea.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
PermissionUtils.requestFileWritePermission(Preferences.this);
|
||||
return true;
|
||||
});
|
||||
chkLogFileNavMessages = (CheckBoxPreference) findPreference(getString(R.string.pref_key_file_navigation_message_output));
|
||||
chkLogFileNavMessages.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
PermissionUtils.requestFileWritePermission(Preferences.this);
|
||||
return true;
|
||||
});
|
||||
chkLogFileMeasurements = (CheckBoxPreference) findPreference(getString(R.string.pref_key_file_measurement_output));
|
||||
chkLogFileMeasurements.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
PermissionUtils.requestFileWritePermission(Preferences.this);
|
||||
return true;
|
||||
});
|
||||
chkLogFileLocation = (CheckBoxPreference) findPreference(getString(R.string.pref_key_file_location_output));
|
||||
chkLogFileLocation.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
PermissionUtils.requestFileWritePermission(Preferences.this);
|
||||
return true;
|
||||
});
|
||||
chkLogFileAntennaJson = (CheckBoxPreference) findPreference(getString(R.string.pref_key_file_antenna_output_json));
|
||||
LocationManager manager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
|
||||
if (SatelliteUtils.isGnssAntennaInfoSupported(manager)) {
|
||||
chkLogFileAntennaJson.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
PermissionUtils.requestFileWritePermission(Preferences.this);
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
// Not supported
|
||||
chkLogFileAntennaJson.setEnabled(false);
|
||||
}
|
||||
chkLogFileAntennaCsv = (CheckBoxPreference) findPreference(getString(R.string.pref_key_file_antenna_output_csv));
|
||||
if (SatelliteUtils.isGnssAntennaInfoSupported(manager)) {
|
||||
chkLogFileAntennaCsv.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
PermissionUtils.requestFileWritePermission(Preferences.this);
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
// Not supported
|
||||
chkLogFileAntennaCsv.setEnabled(false);
|
||||
}
|
||||
|
||||
Application.getPrefs().registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
changePreferenceSummary(getString(R.string.pref_key_preferred_distance_units_v2));
|
||||
changePreferenceSummary(getString(R.string.pref_key_preferred_speed_units_v2));
|
||||
changePreferenceSummary(getString(R.string.pref_key_language));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (key.equalsIgnoreCase(getString(R.string.pref_key_preferred_distance_units_v2))) {
|
||||
// Change the preferred distance units description
|
||||
changePreferenceSummary(key);
|
||||
} else {
|
||||
if (key.equalsIgnoreCase(getString(R.string.pref_key_preferred_speed_units_v2))) {
|
||||
// Change the preferred speed units description
|
||||
changePreferenceSummary(key);
|
||||
} else {
|
||||
if (key.equalsIgnoreCase(getString(R.string.pref_key_language))) {
|
||||
// Change the preferred language description
|
||||
changePreferenceSummary(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
// For dynamically changing the locale
|
||||
super.attachBaseContext(Application.getLocaleManager().setLocale(base));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the value is a valid float
|
||||
*
|
||||
* @param newValue entered value
|
||||
* @return true if its a valid float, false if its not
|
||||
*/
|
||||
private boolean verifyFloat(Object newValue) {
|
||||
try {
|
||||
Float.parseFloat(newValue.toString());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContentView(int layoutResID) {
|
||||
ViewGroup contentView = (ViewGroup) LayoutInflater.from(this).inflate(
|
||||
R.layout.settings_activity, new LinearLayout(this), false);
|
||||
|
||||
mActionBar = (Toolbar) contentView.findViewById(R.id.action_bar);
|
||||
mActionBar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
ViewGroup contentWrapper = (ViewGroup) contentView.findViewById(R.id.content_wrapper);
|
||||
LayoutInflater.from(this).inflate(layoutResID, contentWrapper, true);
|
||||
|
||||
getWindow().setContentView(contentView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the summary of a preference based on a given preference key
|
||||
*
|
||||
* @param prefKey preference key that triggers a change in summary
|
||||
*/
|
||||
private void changePreferenceSummary(String prefKey) {
|
||||
if (prefKey.equalsIgnoreCase(getString(R.string.pref_key_preferred_distance_units_v2))) {
|
||||
String[] values = Application.get().getResources().getStringArray(R.array.preferred_distance_units_values);
|
||||
String[] entries = Application.get().getResources().getStringArray(R.array.preferred_distance_units_entries);
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
if (values[i].equals(preferredDistanceUnits.getValue())) {
|
||||
preferredDistanceUnits.setSummary(entries[i]);
|
||||
}
|
||||
}
|
||||
} else if (prefKey.equalsIgnoreCase(getString(R.string.pref_key_preferred_speed_units_v2))) {
|
||||
String[] values = Application.get().getResources().getStringArray(R.array.preferred_speed_units_values);
|
||||
String[] entries = Application.get().getResources().getStringArray(R.array.preferred_speed_units_entries);
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
if (values[i].equals(preferredSpeedUnits.getValue())) {
|
||||
preferredSpeedUnits.setSummary(entries[i]);
|
||||
}
|
||||
}
|
||||
} else if (prefKey.equalsIgnoreCase(getString(R.string.pref_key_language))) {
|
||||
String[] values = Application.get().getResources().getStringArray(R.array.language_values);
|
||||
String[] entries = Application.get().getResources().getStringArray(R.array.language_entries);
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
if (values[i].equals(language.getValue())) {
|
||||
language.setSummary(entries[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
package com.android.gpstest.io;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.MediaStore.Downloads;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.android.gpstest.Application;
|
||||
import com.android.gpstest.R;
|
||||
|
||||
@@ -12,6 +20,8 @@ import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
@@ -24,9 +34,10 @@ public abstract class BaseFileLogger implements FileLogger {
|
||||
protected final String TAG = this.getClass().getName();
|
||||
protected static final String FILE_PREFIX = "gnss_log";
|
||||
|
||||
protected static final String DIRECTORY = "Download/GPSTest";
|
||||
|
||||
protected final Context context;
|
||||
|
||||
protected final Object fileLock = new Object();
|
||||
protected BufferedWriter fileWriter;
|
||||
protected File file;
|
||||
protected boolean isStarted = false;
|
||||
@@ -77,73 +88,71 @@ public abstract class BaseFileLogger implements FileLogger {
|
||||
* @param date The date and time to use for the file name
|
||||
* @return true if a new file was created, false if an existing file was used
|
||||
*/
|
||||
public boolean startLog(File existingFile, Date date) {
|
||||
public synchronized boolean startLog(File existingFile, Date date) {
|
||||
boolean isNewFile = false;
|
||||
synchronized (fileLock) {
|
||||
String state = Environment.getExternalStorageState();
|
||||
if (Environment.MEDIA_MOUNTED.equals(state)) {
|
||||
baseDirectory = new File(Environment.getExternalStorageDirectory(), FILE_PREFIX);
|
||||
baseDirectory.mkdirs();
|
||||
} else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
|
||||
logError("Cannot write to external storage.");
|
||||
return false;
|
||||
} else {
|
||||
logError("Cannot read external storage.");
|
||||
return false;
|
||||
}
|
||||
|
||||
String currentFilePath;
|
||||
|
||||
if (existingFile != null) {
|
||||
// Use existing file
|
||||
currentFilePath = existingFile.getAbsolutePath();
|
||||
BufferedWriter writer;
|
||||
try {
|
||||
writer = new BufferedWriter(new FileWriter(existingFile, true));
|
||||
} catch (IOException e) {
|
||||
logException("Could not open file: " + currentFilePath, e);
|
||||
return false;
|
||||
}
|
||||
if (!closeOldFileWriter()) {
|
||||
return false;
|
||||
}
|
||||
file = existingFile;
|
||||
fileWriter = writer;
|
||||
isNewFile = false;
|
||||
} else {
|
||||
// Create new logging file
|
||||
SimpleDateFormat formatter = new SimpleDateFormat("yyy_MM_dd_HH_mm_ss");
|
||||
String fileName = String.format("%s_%s." + getFileExtension(), FILE_PREFIX, formatter.format(date));
|
||||
File currentFile = new File(baseDirectory, fileName);
|
||||
currentFilePath = currentFile.getAbsolutePath();
|
||||
BufferedWriter writer;
|
||||
try {
|
||||
writer = new BufferedWriter(new FileWriter(currentFile, true));
|
||||
} catch (IOException e) {
|
||||
logException("Could not open file: " + currentFilePath, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
writeFileHeader(writer, currentFilePath);
|
||||
|
||||
if (!closeOldFileWriter()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
file = currentFile;
|
||||
fileWriter = writer;
|
||||
|
||||
Log.d(TAG, Application.get().getString(R.string.logging_to_new_file, currentFilePath));
|
||||
isNewFile = true;
|
||||
}
|
||||
|
||||
boolean postInit = postFileInit(fileWriter, isNewFile);
|
||||
if (!postInit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
isStarted = true;
|
||||
String state = Environment.getExternalStorageState();
|
||||
if (Environment.MEDIA_MOUNTED.equals(state)) {
|
||||
baseDirectory = new File(context.getExternalFilesDir(null), FILE_PREFIX);
|
||||
baseDirectory.mkdirs();
|
||||
} else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
|
||||
logError("Cannot write to external storage.");
|
||||
return false;
|
||||
} else {
|
||||
logError("Cannot read external storage.");
|
||||
return false;
|
||||
}
|
||||
|
||||
String currentFilePath;
|
||||
|
||||
if (existingFile != null) {
|
||||
// Use existing file
|
||||
currentFilePath = existingFile.getAbsolutePath();
|
||||
BufferedWriter writer;
|
||||
try {
|
||||
writer = new BufferedWriter(new FileWriter(existingFile, true));
|
||||
} catch (IOException e) {
|
||||
logException("Could not open file: " + currentFilePath, e);
|
||||
return false;
|
||||
}
|
||||
if (!closeOldFileWriter()) {
|
||||
return false;
|
||||
}
|
||||
file = existingFile;
|
||||
fileWriter = writer;
|
||||
isNewFile = false;
|
||||
} else {
|
||||
// Create new logging file
|
||||
SimpleDateFormat formatter = new SimpleDateFormat("yyy_MM_dd_HH_mm_ss");
|
||||
String fileName = String.format("%s_%s." + getFileExtension(), FILE_PREFIX, formatter.format(date));
|
||||
File currentFile = new File(baseDirectory, fileName);
|
||||
currentFilePath = currentFile.getAbsolutePath();
|
||||
BufferedWriter writer;
|
||||
try {
|
||||
writer = new BufferedWriter(new FileWriter(currentFile, true));
|
||||
} catch (IOException e) {
|
||||
logException("Could not open file: " + currentFilePath, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
writeFileHeader(writer, currentFilePath);
|
||||
|
||||
if (!closeOldFileWriter()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
file = currentFile;
|
||||
fileWriter = writer;
|
||||
|
||||
Log.d(TAG, Application.Companion.getApp().getString(R.string.logging_to_new_file, currentFilePath));
|
||||
isNewFile = true;
|
||||
}
|
||||
|
||||
boolean postInit = postFileInit(fileWriter, isNewFile);
|
||||
if (!postInit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
isStarted = true;
|
||||
return isNewFile;
|
||||
}
|
||||
|
||||
@@ -152,7 +161,7 @@ public abstract class BaseFileLogger implements FileLogger {
|
||||
try {
|
||||
fileWriter.close();
|
||||
} catch (IOException e) {
|
||||
logException(Application.get().getString(R.string.unable_to_close_all_file_streams), e);
|
||||
logException(Application.Companion.getApp().getString(R.string.unable_to_close_all_file_streams), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -168,7 +177,7 @@ public abstract class BaseFileLogger implements FileLogger {
|
||||
return isStarted;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
public synchronized void close() {
|
||||
if (fileWriter != null) {
|
||||
try {
|
||||
fileWriter.flush();
|
||||
@@ -180,6 +189,10 @@ public abstract class BaseFileLogger implements FileLogger {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && file != null) {
|
||||
copyFileToDownloads(file);
|
||||
}
|
||||
}
|
||||
|
||||
protected void logException(String errorMessage, Exception e) {
|
||||
@@ -191,4 +204,21 @@ public abstract class BaseFileLogger implements FileLogger {
|
||||
Log.e(TAG, errorMessage);
|
||||
Toast.makeText(context, errorMessage, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
protected void copyFileToDownloads(File fileToCopy) {
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(MediaStore.Downloads.DISPLAY_NAME, fileToCopy.getName());
|
||||
contentValues.put(Downloads.RELATIVE_PATH, DIRECTORY);
|
||||
Uri fileUri =
|
||||
contentResolver.insert(
|
||||
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), contentValues);
|
||||
try (OutputStream outputStream =
|
||||
contentResolver.openOutputStream(fileUri)) {
|
||||
Files.copy(fileToCopy.toPath(), outputStream);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error while writing to Downloads folder:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,19 +28,23 @@ import android.location.Location;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
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.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;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* A GNSS logger to store information to a CSV file. Originally from https://github.com/google/gps-measurement-tools/tree/master/GNSSLogger,
|
||||
@@ -63,7 +67,14 @@ public class CsvFileLogger extends BaseFileLogger implements FileLogger {
|
||||
|
||||
@Override
|
||||
boolean postFileInit(BufferedWriter fileWriter, boolean isNewFile) {
|
||||
// No-op for CSV files
|
||||
ContextCompat.getMainExecutor(context).execute(() -> Toast.makeText(
|
||||
Application.Companion.getApp().getApplicationContext(),
|
||||
Application.Companion.getApp().getString(
|
||||
R.string.logging_to_new_file,
|
||||
file.getAbsolutePath()
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -75,6 +86,7 @@ public class CsvFileLogger extends BaseFileLogger implements FileLogger {
|
||||
*/
|
||||
@Override
|
||||
void writeFileHeader(BufferedWriter writer, String filePath) {
|
||||
// TODO - update header to new field formats
|
||||
try {
|
||||
writer.write(COMMENT_START);
|
||||
writer.newLine();
|
||||
@@ -109,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 + ", ");
|
||||
@@ -125,15 +137,7 @@ public class CsvFileLogger extends BaseFileLogger implements FileLogger {
|
||||
writer.newLine();
|
||||
writer.write(COMMENT_START);
|
||||
writer.write(
|
||||
" Raw,ElapsedRealtimeMillis,TimeNanos,LeapSecond,TimeUncertaintyNanos,FullBiasNanos,"
|
||||
+ "BiasNanos,BiasUncertaintyNanos,DriftNanosPerSecond,DriftUncertaintyNanosPerSecond,"
|
||||
+ "HardwareClockDiscontinuityCount,Svid,TimeOffsetNanos,State,ReceivedSvTimeNanos,"
|
||||
+ "ReceivedSvTimeUncertaintyNanos,Cn0DbHz,PseudorangeRateMetersPerSecond,"
|
||||
+ "PseudorangeRateUncertaintyMetersPerSecond,"
|
||||
+ "AccumulatedDeltaRangeState,AccumulatedDeltaRangeMeters,"
|
||||
+ "AccumulatedDeltaRangeUncertaintyMeters,CarrierFrequencyHz,CarrierCycles,"
|
||||
+ "CarrierPhase,CarrierPhaseUncertainty,MultipathIndicator,SnrInDb,"
|
||||
+ "ConstellationType,AgcDb,CarrierFrequencyHz");
|
||||
" Raw,utcTimeMillis,TimeNanos,LeapSecond,TimeUncertaintyNanos,FullBiasNanos,BiasNanos,BiasUncertaintyNanos,DriftNanosPerSecond,DriftUncertaintyNanosPerSecond,HardwareClockDiscontinuityCount,Svid,TimeOffsetNanos,State,ReceivedSvTimeNanos,ReceivedSvTimeUncertaintyNanos,Cn0DbHz,PseudorangeRateMetersPerSecond,PseudorangeRateUncertaintyMetersPerSecond,AccumulatedDeltaRangeState,AccumulatedDeltaRangeMeters,AccumulatedDeltaRangeUncertaintyMeters,CarrierFrequencyHz,CarrierCycles,CarrierPhase,CarrierPhaseUncertainty,MultipathIndicator,SnrInDb,ConstellationType,AgcDb,BasebandCn0DbHz,FullInterSignalBiasNanos,FullInterSignalBiasUncertaintyNanos,SatelliteInterSignalBiasNanos,SatelliteInterSignalBiasUncertaintyNanos,CodeType,ChipsetElapsedRealtimeNanos");
|
||||
writer.newLine();
|
||||
writer.write(COMMENT_START);
|
||||
writer.newLine();
|
||||
@@ -142,7 +146,7 @@ public class CsvFileLogger extends BaseFileLogger implements FileLogger {
|
||||
writer.newLine();
|
||||
writer.write(COMMENT_START);
|
||||
writer.write(
|
||||
" Fix,Provider,Latitude,Longitude,Altitude,Speed,Accuracy,(UTC)TimeInMs");
|
||||
" Fix,Provider,LatitudeDegrees,LongitudeDegrees,AltitudeMeters,SpeedMps,AccuracyMeters,BearingDegrees,UnixTimeMillis,SpeedAccuracyMps,BearingAccuracyDegrees,elapsedRealtimeNanos,VerticalAccuracyMeters,MockLocation");
|
||||
writer.newLine();
|
||||
writer.write(COMMENT_START);
|
||||
writer.newLine();
|
||||
@@ -168,161 +172,142 @@ public class CsvFileLogger extends BaseFileLogger implements FileLogger {
|
||||
writer.write(COMMENT_START);
|
||||
writer.write(" GnssAntennaInfo,CarrierFrequencyMHz,PhaseCenterOffsetXOffsetMm,PhaseCenterOffsetXOffsetUncertaintyMm,PhaseCenterOffsetYOffsetMm,PhaseCenterOffsetYOffsetUncertaintyMm,PhaseCenterOffsetZOffsetMm,PhaseCenterOffsetZOffsetUncertaintyMm,PhaseCenterVariationCorrectionsArray,PhaseCenterVariationCorrectionUncertaintiesArray,PhaseCenterVariationCorrectionsDeltaPhi,PhaseCenterVariationCorrectionsDeltaTheta,SignalGainCorrectionsArray,SignalGainCorrectionUncertaintiesArray,SignalGainCorrectionsDeltaPhi,SignalGainCorrectionsDeltaTheta");
|
||||
writer.newLine();
|
||||
writer.write(COMMENT_START);
|
||||
writer.newLine();
|
||||
writer.write(COMMENT_START);
|
||||
writer.write("GnssStatus format (https://developer.android.com/reference/android/location/GnssStatus):");
|
||||
writer.newLine();
|
||||
writer.write(COMMENT_START);
|
||||
writer.write(" Status,UnixTimeMillis,SignalCount,SignalIndex,ConstellationType,Svid,CarrierFrequencyHz,Cn0DbHz,AzimuthDegrees,ElevationDegrees,UsedInFix,HasAlmanacData,HasEphemerisData,BasebandCn0DbHz");
|
||||
writer.newLine();
|
||||
writer.write(COMMENT_START);
|
||||
writer.write("Orientation sensor format (https://developer.android.com/reference/android/hardware/SensorEvent#values):");
|
||||
writer.newLine();
|
||||
writer.write(COMMENT_START);
|
||||
writer.write(" OrientationDeg,utcTimeMillis,elapsedRealtimeNanos,yawDeg,rollDeg,pitchDeg");
|
||||
writer.newLine();
|
||||
} catch (IOException e) {
|
||||
logException(Application.get().getString(R.string.could_not_initialize_file, filePath), e);
|
||||
logException(Application.Companion.getApp().getString(R.string.could_not_initialize_file, filePath), e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void onLocationChanged(Location location) {
|
||||
public synchronized void onLocationChanged(Location location) {
|
||||
if (location.getProvider().equals(LocationManager.GPS_PROVIDER)) {
|
||||
synchronized (fileLock) {
|
||||
if (fileWriter == null) {
|
||||
return;
|
||||
}
|
||||
String locationStream =
|
||||
String.format(
|
||||
Locale.US,
|
||||
"Fix,%s,%f,%f,%f,%f,%f,%d",
|
||||
location.getProvider(),
|
||||
location.getLatitude(),
|
||||
location.getLongitude(),
|
||||
location.getAltitude(),
|
||||
location.getSpeed(),
|
||||
location.getAccuracy(),
|
||||
location.getTime());
|
||||
try {
|
||||
fileWriter.write(locationStream);
|
||||
fileWriter.newLine();
|
||||
} catch (IOException e) {
|
||||
logException(Application.get().getString(R.string.error_writing_file), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public void onGnssMeasurementsReceived(GnssMeasurementsEvent event) {
|
||||
synchronized (fileLock) {
|
||||
if (fileWriter == null) {
|
||||
return;
|
||||
}
|
||||
GnssClock gnssClock = event.getClock();
|
||||
for (GnssMeasurement measurement : event.getMeasurements()) {
|
||||
try {
|
||||
writeGnssMeasurementToFile(gnssClock, measurement);
|
||||
} catch (IOException e) {
|
||||
logException(Application.get().getString(R.string.error_writing_file), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public void onGnssNavigationMessageReceived(GnssNavigationMessage navigationMessage) {
|
||||
synchronized (fileLock) {
|
||||
if (fileWriter == null) {
|
||||
return;
|
||||
}
|
||||
StringBuilder builder = new StringBuilder("Nav");
|
||||
builder.append(RECORD_DELIMITER);
|
||||
builder.append(navigationMessage.getSvid());
|
||||
builder.append(RECORD_DELIMITER);
|
||||
builder.append(navigationMessage.getType());
|
||||
builder.append(RECORD_DELIMITER);
|
||||
|
||||
int status = navigationMessage.getStatus();
|
||||
builder.append(status);
|
||||
builder.append(RECORD_DELIMITER);
|
||||
builder.append(navigationMessage.getMessageId());
|
||||
builder.append(RECORD_DELIMITER);
|
||||
builder.append(navigationMessage.getSubmessageId());
|
||||
byte[] data = navigationMessage.getData();
|
||||
for (byte word : data) {
|
||||
builder.append(RECORD_DELIMITER);
|
||||
builder.append(word);
|
||||
}
|
||||
String locationStream = FormatUtils.toLog(location);
|
||||
try {
|
||||
fileWriter.write(builder.toString());
|
||||
fileWriter.write(locationStream);
|
||||
fileWriter.newLine();
|
||||
} catch (IOException e) {
|
||||
logException(Application.get().getString(R.string.error_writing_file), e);
|
||||
logException(Application.Companion.getApp().getString(R.string.error_writing_file), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onNmeaReceived(long timestamp, String s) {
|
||||
synchronized (fileLock) {
|
||||
if (fileWriter == null) {
|
||||
return;
|
||||
}
|
||||
String nmeaStream = "NMEA," + s.trim() + "," + timestamp;
|
||||
/**
|
||||
* Called to log GnssStatus information
|
||||
* @param statuses GnssStatus information converted to a list of SatelliteStatus
|
||||
* @param location the most recently calculated location, or null if one hasn't been calculated yet
|
||||
*/
|
||||
public synchronized void onGnssStatusChanged(List<SatelliteStatus> statuses, Location location) {
|
||||
if (fileWriter == null) {
|
||||
return;
|
||||
}
|
||||
int i = 0;
|
||||
for (SatelliteStatus s : statuses) {
|
||||
try {
|
||||
fileWriter.write(nmeaStream);
|
||||
fileWriter.newLine();
|
||||
writeStatusToFile(s, location != null ? location.getTime() : 0, statuses.size(), i);
|
||||
} catch (IOException e) {
|
||||
logException(Application.get().getString(R.string.error_writing_file), e);
|
||||
logException(Application.Companion.getApp().getString(R.string.error_writing_file), e);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void writeStatusToFile(SatelliteStatus status, long unixTimeMillis, int signalCount, int signalIndex) throws IOException {
|
||||
fileWriter.write(
|
||||
FormatUtils.toLog(status, unixTimeMillis, signalCount, signalIndex)
|
||||
);
|
||||
fileWriter.newLine();
|
||||
}
|
||||
|
||||
public synchronized void onGnssMeasurementsReceived(GnssMeasurementsEvent event) {
|
||||
if (fileWriter == null) {
|
||||
return;
|
||||
}
|
||||
GnssClock gnssClock = event.getClock();
|
||||
for (GnssMeasurement measurement : event.getMeasurements()) {
|
||||
try {
|
||||
writeGnssMeasurementToFile(gnssClock, measurement);
|
||||
} catch (IOException e) {
|
||||
logException(Application.Companion.getApp().getString(R.string.error_writing_file), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public synchronized void onGnssNavigationMessageReceived(GnssNavigationMessage navigationMessage) {
|
||||
if (fileWriter == null) {
|
||||
return;
|
||||
}
|
||||
StringBuilder builder = new StringBuilder("Nav");
|
||||
builder.append(RECORD_DELIMITER);
|
||||
builder.append(navigationMessage.getSvid());
|
||||
builder.append(RECORD_DELIMITER);
|
||||
builder.append(navigationMessage.getType());
|
||||
builder.append(RECORD_DELIMITER);
|
||||
|
||||
int status = navigationMessage.getStatus();
|
||||
builder.append(status);
|
||||
builder.append(RECORD_DELIMITER);
|
||||
builder.append(navigationMessage.getMessageId());
|
||||
builder.append(RECORD_DELIMITER);
|
||||
builder.append(navigationMessage.getSubmessageId());
|
||||
byte[] data = navigationMessage.getData();
|
||||
for (byte word : data) {
|
||||
builder.append(RECORD_DELIMITER);
|
||||
builder.append(word);
|
||||
}
|
||||
try {
|
||||
fileWriter.write(builder.toString());
|
||||
fileWriter.newLine();
|
||||
} catch (IOException e) {
|
||||
logException(Application.Companion.getApp().getString(R.string.error_writing_file), e);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void onNmeaReceived(long timestamp, String s) {
|
||||
if (fileWriter == null) {
|
||||
return;
|
||||
}
|
||||
String nmeaStream = "NMEA," + s.trim() + "," + timestamp;
|
||||
try {
|
||||
fileWriter.write(nmeaStream);
|
||||
fileWriter.newLine();
|
||||
} catch (IOException e) {
|
||||
logException(Application.Companion.getApp().getString(R.string.error_writing_file), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeGnssMeasurementToFile(GnssClock clock, GnssMeasurement measurement)
|
||||
throws IOException {
|
||||
String clockStream =
|
||||
String.format(
|
||||
"Raw,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s",
|
||||
fileWriter.write(
|
||||
FormatUtils.toLog(
|
||||
SystemClock.elapsedRealtime(),
|
||||
clock.getTimeNanos(),
|
||||
clock.hasLeapSecond() ? clock.getLeapSecond() : "",
|
||||
clock.hasTimeUncertaintyNanos() ? clock.getTimeUncertaintyNanos() : "",
|
||||
clock.getFullBiasNanos(),
|
||||
clock.hasBiasNanos() ? clock.getBiasNanos() : "",
|
||||
clock.hasBiasUncertaintyNanos() ? clock.getBiasUncertaintyNanos() : "",
|
||||
clock.hasDriftNanosPerSecond() ? clock.getDriftNanosPerSecond() : "",
|
||||
clock.hasDriftUncertaintyNanosPerSecond()
|
||||
? clock.getDriftUncertaintyNanosPerSecond()
|
||||
: "",
|
||||
clock.getHardwareClockDiscontinuityCount() + ",");
|
||||
fileWriter.write(clockStream);
|
||||
|
||||
String measurementStream =
|
||||
String.format(
|
||||
"%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s",
|
||||
measurement.getSvid(),
|
||||
measurement.getTimeOffsetNanos(),
|
||||
measurement.getState(),
|
||||
measurement.getReceivedSvTimeNanos(),
|
||||
measurement.getReceivedSvTimeUncertaintyNanos(),
|
||||
measurement.getCn0DbHz(),
|
||||
measurement.getPseudorangeRateMetersPerSecond(),
|
||||
measurement.getPseudorangeRateUncertaintyMetersPerSecond(),
|
||||
measurement.getAccumulatedDeltaRangeState(),
|
||||
measurement.getAccumulatedDeltaRangeMeters(),
|
||||
measurement.getAccumulatedDeltaRangeUncertaintyMeters(),
|
||||
measurement.hasCarrierFrequencyHz() ? measurement.getCarrierFrequencyHz() : "",
|
||||
measurement.hasCarrierCycles() ? measurement.getCarrierCycles() : "",
|
||||
measurement.hasCarrierPhase() ? measurement.getCarrierPhase() : "",
|
||||
measurement.hasCarrierPhaseUncertainty()
|
||||
? measurement.getCarrierPhaseUncertainty()
|
||||
: "",
|
||||
measurement.getMultipathIndicator(),
|
||||
measurement.hasSnrInDb() ? measurement.getSnrInDb() : "",
|
||||
measurement.getConstellationType(),
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||
&& measurement.hasAutomaticGainControlLevelDb()
|
||||
? measurement.getAutomaticGainControlLevelDb()
|
||||
: "",
|
||||
measurement.hasCarrierFrequencyHz() ? measurement.getCarrierFrequencyHz() : "");
|
||||
fileWriter.write(measurementStream);
|
||||
SystemClock.elapsedRealtimeNanos(),
|
||||
clock,
|
||||
measurement)
|
||||
);
|
||||
fileWriter.newLine();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.R)
|
||||
public void onGnssAntennaInfoReceived(@NonNull List<GnssAntennaInfo> list) {
|
||||
public synchronized void onGnssAntennaInfoReceived(@NonNull List<GnssAntennaInfo> list) {
|
||||
try {
|
||||
for (GnssAntennaInfo info : list) {
|
||||
fileWriter.write(IOUtils.serialize(info));
|
||||
fileWriter.write(FormatUtils.toLog(info));
|
||||
fileWriter.newLine();
|
||||
}
|
||||
fileWriter.newLine();
|
||||
@@ -330,4 +315,16 @@ public class CsvFileLogger extends BaseFileLogger implements FileLogger {
|
||||
logException("Unable to write antenna info to CSV", e);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void onOrientationChanged(Orientation orientation, long currentTimeMs, long millisSinceBootMs) {
|
||||
if (fileWriter == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
fileWriter.write(FormatUtils.toLog(orientation, currentTimeMs, millisSinceBootMs));
|
||||
fileWriter.newLine();
|
||||
} catch (IOException e) {
|
||||
logException(Application.Companion.getApp().getString(R.string.error_writing_file), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ class DevicePropertiesUploader(private val inputData: Bundle) {
|
||||
}
|
||||
|
||||
private fun buildUri(): Uri {
|
||||
return Uri.parse(Application.get().resources.getString(R.string.device_properties_upload_url)).buildUpon()
|
||||
return Uri.parse(Application.app.resources.getString(R.string.device_properties_upload_url)).buildUpon()
|
||||
.appendQueryParameter(MANUFACTURER, inputData.getString(MANUFACTURER))
|
||||
.appendQueryParameter(MODEL, inputData.getString(MODEL))
|
||||
.appendQueryParameter(DEVICE, inputData.getString(DEVICE))
|
||||
@@ -105,7 +105,7 @@ class DevicePropertiesUploader(private val inputData: Bundle) {
|
||||
if (result != null) {
|
||||
Log.e(TAG, result)
|
||||
}
|
||||
Log.e(TAG, Application.get().getString(R.string.upload_failure))
|
||||
Log.e(TAG, Application.app.getString(R.string.upload_failure))
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -19,9 +19,11 @@ package com.android.gpstest.io;
|
||||
import android.content.Context;
|
||||
import android.location.GnssAntennaInfo;
|
||||
import android.os.Build;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.android.gpstest.Application;
|
||||
import com.android.gpstest.R;
|
||||
@@ -66,10 +68,18 @@ public class JsonFileLogger extends BaseFileLogger implements FileLogger {
|
||||
jsonGenerator.writeStartArray();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logException(Application.get().getString(R.string.unable_to_open_json_generator), e);
|
||||
logException(Application.Companion.getApp().getString(R.string.unable_to_open_json_generator), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ContextCompat.getMainExecutor(context).execute(() -> Toast.makeText(
|
||||
Application.Companion.getApp().getApplicationContext(),
|
||||
Application.Companion.getApp().getString(
|
||||
R.string.logging_to_new_file,
|
||||
file.getAbsolutePath()
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -82,7 +92,7 @@ public class JsonFileLogger extends BaseFileLogger implements FileLogger {
|
||||
* @return true if a new file was created, false if an existing file was used
|
||||
*/
|
||||
@Override
|
||||
public boolean startLog(File existingFile, Date date) {
|
||||
public synchronized boolean startLog(File existingFile, Date date) {
|
||||
if (mapper == null) {
|
||||
mapper = new ObjectMapper();
|
||||
// We manage closing the underlying file streams in super.close()
|
||||
@@ -92,7 +102,7 @@ public class JsonFileLogger extends BaseFileLogger implements FileLogger {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
public synchronized void close() {
|
||||
if (fileWriter != null) {
|
||||
try {
|
||||
if (jsonGenerator != null) {
|
||||
@@ -110,7 +120,7 @@ public class JsonFileLogger extends BaseFileLogger implements FileLogger {
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.R)
|
||||
public void onGnssAntennaInfoReceived(@NonNull List<GnssAntennaInfo> list) {
|
||||
public synchronized void onGnssAntennaInfoReceived(@NonNull List<GnssAntennaInfo> list) {
|
||||
try {
|
||||
if (mapper != null && jsonGenerator != null) {
|
||||
for (GnssAntennaInfo info : list) {
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
|
||||
package com.android.gpstest.lang;
|
||||
|
||||
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
|
||||
import static android.os.Build.VERSION_CODES.N;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
@@ -14,13 +17,10 @@ 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;
|
||||
|
||||
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
|
||||
import static android.os.Build.VERSION_CODES.N;
|
||||
|
||||
/**
|
||||
* Dynamically changes the app locale
|
||||
*/
|
||||
@@ -54,7 +54,7 @@ public class LocaleManager {
|
||||
private void persistLanguage(String language) {
|
||||
// use commit() instead of apply(), because sometimes we kill the application process immediately
|
||||
// which will prevent apply() to finish
|
||||
prefs.edit().putString(Application.get().getString(R.string.pref_key_language), language).commit();
|
||||
prefs.edit().putString(Application.Companion.getApp().getString(R.string.pref_key_language), language).commit();
|
||||
}
|
||||
|
||||
private Context updateResources(Context context, String language) {
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
package com.android.gpstest.map;
|
||||
|
||||
import android.location.Location;
|
||||
import android.os.Bundle;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.android.gpstest.BenchmarkViewModel;
|
||||
import com.android.gpstest.model.MeasuredError;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import static com.android.gpstest.map.MapConstants.ALLOW_GROUND_TRUTH_CHANGE;
|
||||
import static com.android.gpstest.map.MapConstants.GROUND_TRUTH;
|
||||
import static com.android.gpstest.map.MapConstants.MODE;
|
||||
import static com.android.gpstest.map.MapConstants.MODE_ACCURACY;
|
||||
import static com.android.gpstest.map.MapConstants.MODE_MAP;
|
||||
|
||||
import android.location.Location;
|
||||
import android.os.Bundle;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.android.gpstest.library.model.MeasuredError;
|
||||
import com.android.gpstest.ui.BenchmarkViewModel;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public class MapViewModelController {
|
||||
|
||||
/**
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.model
|
||||
|
||||
/**
|
||||
* A container class that holds metadata and statistics information about a group of satellites.
|
||||
* Summary statistics on the constellation family such as the number of signals in view
|
||||
* ([numSignalsInView]), number of signals used in the fix ([numSignalsUsed], and the number
|
||||
* of satellites used in the fix ([numSatsUsed]), and the number of satellites in view ([numSatsInView])
|
||||
*/
|
||||
data class SatelliteMetadata(
|
||||
val numSignalsInView: Int,
|
||||
val numSignalsUsed: Int,
|
||||
val numSignalsTotal: Int,
|
||||
val numSatsInView: Int,
|
||||
val numSatsUsed: Int,
|
||||
val numSatsTotal: Int)
|
||||
@@ -13,14 +13,14 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.gpstest;
|
||||
package com.android.gpstest.ui;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
/**
|
||||
* An interface for controlling the Benchmark feature
|
||||
*/
|
||||
interface BenchmarkController extends GpsTestListener {
|
||||
interface BenchmarkController {
|
||||
|
||||
/**
|
||||
* Called when there is a map click on a location so the controller can be updated with that information
|
||||
@@ -42,6 +42,11 @@ interface BenchmarkController extends GpsTestListener {
|
||||
*/
|
||||
void onResume();
|
||||
|
||||
/**
|
||||
* Called from the hosting Activity when a new location should be added to the benchmark session
|
||||
*/
|
||||
void onLocationChanged(Location location);
|
||||
|
||||
/**
|
||||
* Show the Benchmark views
|
||||
*/
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.gpstest;
|
||||
package com.android.gpstest.ui;
|
||||
|
||||
import static android.text.TextUtils.isEmpty;
|
||||
import static android.view.View.GONE;
|
||||
@@ -24,12 +24,8 @@ import android.animation.LayoutTransition;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.location.GnssMeasurementsEvent;
|
||||
import android.location.GnssStatus;
|
||||
import android.location.GpsStatus;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.util.TypedValue;
|
||||
@@ -47,12 +43,15 @@ import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.Observer;
|
||||
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.LibUIUtils;
|
||||
import com.android.gpstest.library.util.MathUtils;
|
||||
import com.android.gpstest.library.util.PreferenceUtils;
|
||||
import com.android.gpstest.util.UIUtils;
|
||||
import com.github.mikephil.charting.charts.LineChart;
|
||||
import com.github.mikephil.charting.components.Legend;
|
||||
@@ -111,7 +110,7 @@ public class BenchmarkControllerImpl implements BenchmarkController {
|
||||
|
||||
String mPrefDistanceUnits;
|
||||
|
||||
private static final String METERS = Application.get().getResources().getStringArray(R.array.preferred_distance_units_values)[0];
|
||||
private static final String METERS = Application.Companion.getApp().getResources().getStringArray(R.array.preferred_distance_units_values)[0];
|
||||
|
||||
BenchmarkViewModel mViewModel;
|
||||
|
||||
@@ -121,24 +120,24 @@ public class BenchmarkControllerImpl implements BenchmarkController {
|
||||
if (!allowEdit) {
|
||||
if (mViewModel.getGroundTruthLocation().getValue().hasAltitude()) {
|
||||
// Set default text size and align units properly
|
||||
mErrorView.setTextSize(TypedValue.COMPLEX_UNIT_PX, Application.get().getResources().getDimension(R.dimen.ground_truth_sliding_header_vert_text_size));
|
||||
mAvgErrorView.setTextSize(TypedValue.COMPLEX_UNIT_PX, Application.get().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);
|
||||
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));
|
||||
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.get().getResources().getDimension(R.dimen.ground_truth_sliding_header_error_text_size));
|
||||
mAvgErrorView.setTextSize(TypedValue.COMPLEX_UNIT_PX, Application.get().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);
|
||||
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));
|
||||
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
|
||||
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mGroundTruthCardView.getLayoutParams();
|
||||
mMotionLayout.transitionToEnd();
|
||||
lp.height = (int) Application.get().getResources().getDimension(R.dimen.ground_truth_cardview_height_collapsed);
|
||||
lp.height = (int) Application.Companion.getApp().getResources().getDimension(R.dimen.ground_truth_cardview_height_collapsed);
|
||||
mGroundTruthCardView.setLayoutParams(lp);
|
||||
|
||||
// Show sliding panel if we're showing the Accuracy fragment and the sliding panel isn't visible
|
||||
@@ -155,7 +154,7 @@ public class BenchmarkControllerImpl implements BenchmarkController {
|
||||
// Expand card to allow editing ground truth
|
||||
mMotionLayout.transitionToStart();
|
||||
// We have to set height on card manually because it doesn't auto-expand right when views are within card container
|
||||
lp.height = (int) Application.get().getResources().getDimension(R.dimen.ground_truth_cardview_height);
|
||||
lp.height = (int) Application.Companion.getApp().getResources().getDimension(R.dimen.ground_truth_cardview_height);
|
||||
mGroundTruthCardView.setLayoutParams(lp);
|
||||
|
||||
// Collapse sliding panel if it's anchored so there is room
|
||||
@@ -178,12 +177,12 @@ public class BenchmarkControllerImpl implements BenchmarkController {
|
||||
mErrorUnit.setVisibility(VISIBLE);
|
||||
mErrorView.setVisibility(VISIBLE);
|
||||
if (mPrefDistanceUnits.equalsIgnoreCase(METERS)) {
|
||||
mErrorView.setText(Application.get().getString(R.string.benchmark_error, error.getError()));
|
||||
mErrorUnit.setText(Application.get().getString(R.string.meters_abbreviation));
|
||||
mErrorView.setText(Application.Companion.getApp().getString(R.string.benchmark_error, error.getError()));
|
||||
mErrorUnit.setText(Application.Companion.getApp().getString(R.string.meters_abbreviation));
|
||||
} else {
|
||||
// Feet
|
||||
mErrorView.setText(Application.get().getString(R.string.benchmark_error, UIUtils.toFeet(error.getError())));
|
||||
mErrorUnit.setText(Application.get().getString(R.string.feet_abbreviation));
|
||||
mErrorView.setText(Application.Companion.getApp().getString(R.string.benchmark_error, LibUIUtils.toFeet(error.getError())));
|
||||
mErrorUnit.setText(Application.Companion.getApp().getString(R.string.feet_abbreviation));
|
||||
}
|
||||
}
|
||||
if (mVertErrorView != null && !Double.isNaN(error.getVertError())) {
|
||||
@@ -193,10 +192,10 @@ public class BenchmarkControllerImpl implements BenchmarkController {
|
||||
mRightDivider.setVisibility(VISIBLE);
|
||||
mVertErrorView.setVisibility(VISIBLE);
|
||||
if (mPrefDistanceUnits.equalsIgnoreCase(METERS)) {
|
||||
mVertErrorView.setText(Application.get().getString(R.string.benchmark_error, Math.abs(error.getVertError())));
|
||||
mVertErrorView.setText(Application.Companion.getApp().getString(R.string.benchmark_error, Math.abs(error.getVertError())));
|
||||
} else {
|
||||
// Feet
|
||||
mVertErrorView.setText(Application.get().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 {
|
||||
@@ -218,22 +217,22 @@ public class BenchmarkControllerImpl implements BenchmarkController {
|
||||
mAvgErrorUnit.setVisibility(VISIBLE);
|
||||
mAvgErrorView.setVisibility(VISIBLE);
|
||||
if (mPrefDistanceUnits.equalsIgnoreCase(METERS)) {
|
||||
mAvgErrorView.setText(Application.get().getString(R.string.benchmark_error, avgError.getAvgError()));
|
||||
mAvgErrorUnit.setText(Application.get().getString(R.string.meters_abbreviation));
|
||||
mAvgErrorView.setText(Application.Companion.getApp().getString(R.string.benchmark_error, avgError.getAvgError()));
|
||||
mAvgErrorUnit.setText(Application.Companion.getApp().getString(R.string.meters_abbreviation));
|
||||
} else {
|
||||
// Feet
|
||||
mAvgErrorView.setText(Application.get().getString(R.string.benchmark_error, UIUtils.toFeet(avgError.getAvgError())));
|
||||
mAvgErrorUnit.setText(Application.get().getString(R.string.feet_abbreviation));
|
||||
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.get().getString(R.string.avg_error_label, avgError.getCount()));
|
||||
mAvgErrorLabel.setText(Application.Companion.getApp().getString(R.string.avg_error_label, avgError.getCount()));
|
||||
}
|
||||
if (mAvgVertErrorView != null && !Double.isNaN(avgError.getAvgVertAbsError())) {
|
||||
// Vertical errors
|
||||
mAvgVertErrorView.setVisibility(VISIBLE);
|
||||
if (mPrefDistanceUnits.equalsIgnoreCase(METERS)) {
|
||||
mAvgVertErrorView.setText(Application.get().getString(R.string.benchmark_error, avgError.getAvgVertAbsError()));
|
||||
mAvgVertErrorView.setText(Application.Companion.getApp().getString(R.string.benchmark_error, avgError.getAvgVertAbsError()));
|
||||
} else {
|
||||
mAvgVertErrorView.setText(Application.get().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
|
||||
@@ -243,7 +242,7 @@ public class BenchmarkControllerImpl implements BenchmarkController {
|
||||
};
|
||||
|
||||
public BenchmarkControllerImpl(AppCompatActivity activity, View v) {
|
||||
if (Application.getPrefs().getBoolean(activity.getString(R.string.pref_key_dark_theme), false)) {
|
||||
if (Application.Companion.getPrefs().getBoolean(activity.getString(R.string.pref_key_dark_theme), false)) {
|
||||
// Dark theme
|
||||
mChartTextColor = ContextCompat.getColor(activity, R.color.body_text_1_dark);
|
||||
} else {
|
||||
@@ -258,7 +257,7 @@ public class BenchmarkControllerImpl implements BenchmarkController {
|
||||
mAvgVertErrorView = v.findViewById(R.id.avg_vert_error);
|
||||
mErrorLabel = v.findViewById(R.id.error_label);
|
||||
mAvgErrorLabel = v.findViewById(R.id.avg_error_label);
|
||||
mAvgErrorLabel.setText(Application.get().getString(R.string.avg_error_label, 0));
|
||||
mAvgErrorLabel.setText(Application.Companion.getApp().getString(R.string.avg_error_label, 0));
|
||||
mLeftDivider = v.findViewById(R.id.divider_left);
|
||||
mRightDivider = v.findViewById(R.id.divider_right);
|
||||
mErrorUnit = v.findViewById(R.id.error_unit);
|
||||
@@ -316,8 +315,8 @@ public class BenchmarkControllerImpl implements BenchmarkController {
|
||||
});
|
||||
|
||||
mQrCode.setOnClickListener(view -> {
|
||||
if (!Application.getPrefs().getBoolean(
|
||||
Application.get().getString(R.string.pref_key_never_show_qr_code_instructions), false)) {
|
||||
if (!Application.Companion.getPrefs().getBoolean(
|
||||
Application.Companion.getApp().getString(R.string.pref_key_never_show_qr_code_instructions), false)) {
|
||||
UIUtils.createQrCodeDialog(activity).show();
|
||||
} else {
|
||||
IOUtils.openQrCodeReader(activity);
|
||||
@@ -339,24 +338,24 @@ 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) {
|
||||
Toast.makeText(activity, Application.get().getString(R.string.show_radar_valid_location), Toast.LENGTH_LONG).show();
|
||||
Toast.makeText(activity, Application.Companion.getApp().getString(R.string.show_radar_valid_location), Toast.LENGTH_LONG).show();
|
||||
restoreGroundTruth(groundTruth);
|
||||
} else {
|
||||
Toast.makeText(activity, Application.get().getString(R.string.show_radar_invalid_location), Toast.LENGTH_LONG).show();
|
||||
Toast.makeText(activity, Application.Companion.getApp().getString(R.string.show_radar_invalid_location), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
} else if (Application.getPrefs().contains(GROUND_TRUTH_LAT)) {
|
||||
} 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));
|
||||
if (Application.getPrefs().contains(GROUND_TRUTH_ALT)) {
|
||||
groundTruth.setAltitude(PreferenceUtils.getDouble(GROUND_TRUTH_ALT, 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, Application.Companion.getPrefs()));
|
||||
}
|
||||
restoreGroundTruth(groundTruth);
|
||||
}
|
||||
@@ -379,7 +378,7 @@ public class BenchmarkControllerImpl implements BenchmarkController {
|
||||
* Should be called when the ground truth card state is fully expanded
|
||||
*/
|
||||
private void onCardExpanded() {
|
||||
mSaveGroundTruth.setText(Application.get().getString(R.string.save));
|
||||
mSaveGroundTruth.setText(Application.Companion.getApp().getString(R.string.save));
|
||||
mLatText.setEnabled(true);
|
||||
mLongText.setEnabled(true);
|
||||
mAltText.setEnabled(true);
|
||||
@@ -392,7 +391,7 @@ public class BenchmarkControllerImpl implements BenchmarkController {
|
||||
* Should be called when the ground truth card state is fully collapsed
|
||||
*/
|
||||
private void onCardCollapsed() {
|
||||
mSaveGroundTruth.setText(Application.get().getString(R.string.edit));
|
||||
mSaveGroundTruth.setText(Application.Companion.getApp().getString(R.string.edit));
|
||||
mLatText.setEnabled(false);
|
||||
mLongText.setEnabled(false);
|
||||
mAltText.setEnabled(false);
|
||||
@@ -420,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,9 +486,9 @@ public class BenchmarkControllerImpl implements BenchmarkController {
|
||||
private void initChartUnits(LineChart errorChart) {
|
||||
String unit;
|
||||
if (mPrefDistanceUnits.equalsIgnoreCase(METERS)) {
|
||||
unit = Application.get().getString(R.string.meters_abbreviation);
|
||||
unit = Application.Companion.getApp().getString(R.string.meters_abbreviation);
|
||||
} else {
|
||||
unit = Application.get().getString(R.string.feet_abbreviation);
|
||||
unit = Application.Companion.getApp().getString(R.string.feet_abbreviation);
|
||||
}
|
||||
|
||||
DistanceValueFormatter formatter = new DistanceValueFormatter(unit);
|
||||
@@ -506,7 +505,7 @@ public class BenchmarkControllerImpl implements BenchmarkController {
|
||||
mRightDivider.setVisibility(INVISIBLE);
|
||||
mErrorUnit.setVisibility(INVISIBLE);
|
||||
mAvgErrorUnit.setVisibility(INVISIBLE);
|
||||
mAvgErrorLabel.setText(Application.get().getString(R.string.avg_error_label, 0));
|
||||
mAvgErrorLabel.setText(Application.Companion.getApp().getString(R.string.avg_error_label, 0));
|
||||
|
||||
mErrorChart.clearValues();
|
||||
mVertErrorChart.clearValues();
|
||||
@@ -579,57 +578,6 @@ public class BenchmarkControllerImpl implements BenchmarkController {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void gpsStart() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void gpsStop() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGpsStatusChanged(int event, GpsStatus status) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssFirstFix(int ttffMillis) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSatelliteStatusChanged(GnssStatus status) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssStarted() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssStopped() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssMeasurementsReceived(GnssMeasurementsEvent event) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOrientationChanged(double orientation, double tilt) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNmeaMessage(String message, long timestamp) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationChanged(Location location) {
|
||||
mViewModel.addLocation(location);
|
||||
}
|
||||
@@ -648,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);
|
||||
|
||||
@@ -660,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -722,9 +670,9 @@ public class BenchmarkControllerImpl implements BenchmarkController {
|
||||
private LineDataSet createGraphDataSet(int setType) {
|
||||
String label;
|
||||
if (setType == ERROR_SET) {
|
||||
label = Application.get().getResources().getString(R.string.measured_error_graph_label);
|
||||
label = Application.Companion.getApp().getResources().getString(R.string.measured_error_graph_label);
|
||||
} else {
|
||||
label = Application.get().getResources().getString(R.string.estimated_accuracy_graph_label);
|
||||
label = Application.Companion.getApp().getResources().getString(R.string.estimated_accuracy_graph_label);
|
||||
}
|
||||
|
||||
LineDataSet set = new LineDataSet(null, label);
|
||||
@@ -745,31 +693,6 @@ public class BenchmarkControllerImpl implements BenchmarkController {
|
||||
return set;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(String s, int i, Bundle bundle) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderEnabled(String s) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderDisabled(String s) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssFixAcquired() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssFixLost() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMapClick(Location location) {
|
||||
if (!mViewModel.getBenchmarkCardCollapsed()) {
|
||||
@@ -778,11 +701,11 @@ public class BenchmarkControllerImpl implements BenchmarkController {
|
||||
}
|
||||
|
||||
private void updateGroundTruthEditTexts(Location location) {
|
||||
mLatText.getEditText().setText(Application.get().getString(R.string.benchmark_lat_long, location.getLatitude()));
|
||||
mLongText.getEditText().setText(Application.get().getString(R.string.benchmark_lat_long, location.getLongitude()));
|
||||
mLatText.getEditText().setText(Application.Companion.getApp().getString(R.string.benchmark_lat_long, location.getLatitude()));
|
||||
mLongText.getEditText().setText(Application.Companion.getApp().getString(R.string.benchmark_lat_long, location.getLongitude()));
|
||||
|
||||
if (location.hasAltitude()) {
|
||||
mAltText.getEditText().setText(Application.get().getString(R.string.benchmark_alt, location.getAltitude()));
|
||||
mAltText.getEditText().setText(Application.Companion.getApp().getString(R.string.benchmark_alt, location.getAltitude()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -795,8 +718,8 @@ public class BenchmarkControllerImpl implements BenchmarkController {
|
||||
}
|
||||
|
||||
private void setupUnitPreferences() {
|
||||
SharedPreferences settings = Application.getPrefs();
|
||||
Application app = Application.get();
|
||||
SharedPreferences settings = Application.Companion.getPrefs();
|
||||
Application app = Application.Companion.getApp();
|
||||
|
||||
String prefDistanceUnits = settings
|
||||
.getString(app.getString(R.string.pref_key_preferred_distance_units_v2), METERS);
|
||||
@@ -869,7 +792,7 @@ public class BenchmarkControllerImpl implements BenchmarkController {
|
||||
float newOffset = MathUtils.mapToRange(slideOffset, ANIMATE_THRESHOLD_PERCENT, 1.0f, 0f, 1.0f);
|
||||
GradientDrawable shape = new GradientDrawable();
|
||||
float[] corners = new float[8];
|
||||
float radius = (1 - newOffset) * Application.get().getResources().getDimensionPixelSize(R.dimen.ground_truth_sliding_header_corner_radius);
|
||||
float radius = (1 - newOffset) * Application.Companion.getApp().getResources().getDimensionPixelSize(R.dimen.ground_truth_sliding_header_corner_radius);
|
||||
corners[0] = radius;
|
||||
corners[1] = radius;
|
||||
corners[2] = radius;
|
||||
@@ -879,7 +802,7 @@ public class BenchmarkControllerImpl implements BenchmarkController {
|
||||
corners[6] = 0;
|
||||
corners[7] = 0;
|
||||
shape.setCornerRadii(corners);
|
||||
shape.setColor(Application.get().getResources().getColor(R.color.colorPrimary));
|
||||
shape.setColor(Application.Companion.getApp().getResources().getColor(R.color.colorPrimary));
|
||||
mSlidingPanelHeader.setBackground(shape);
|
||||
}
|
||||
}
|
||||
@@ -13,24 +13,24 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.gpstest;
|
||||
package com.android.gpstest.ui;
|
||||
|
||||
import android.app.Application;
|
||||
import android.location.Location;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.android.gpstest.model.AvgError;
|
||||
import com.android.gpstest.model.MeasuredError;
|
||||
import com.android.gpstest.util.BenchmarkUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
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 model that holds GNSS benchmarking (ground truth and error measurement) information
|
||||
*/
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.android.gpstest.ui;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
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.library.model.GnssType;
|
||||
import com.android.gpstest.library.util.PreferenceUtils;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class GnssFilterDialog extends DialogFragment
|
||||
implements DialogInterface.OnMultiChoiceClickListener,
|
||||
DialogInterface.OnClickListener {
|
||||
|
||||
public static final String ITEMS = ".items";
|
||||
|
||||
public static final String CHECKS = ".checks";
|
||||
|
||||
private boolean[] mChecks;
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
Bundle args = getArguments();
|
||||
String[] items = args.getStringArray(ITEMS);
|
||||
mChecks = args.getBooleanArray(CHECKS);
|
||||
if (savedInstanceState != null) {
|
||||
mChecks = savedInstanceState.getBooleanArray(CHECKS);
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
return builder.setTitle(R.string.filter_dialog_title)
|
||||
.setMultiChoiceItems(items, mChecks, this)
|
||||
.setPositiveButton(R.string.save, this)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
outState.putBooleanArray(CHECKS, mChecks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Set<GnssType> filter = new LinkedHashSet<>();
|
||||
GnssType[] gnssTypes = GnssType.values();
|
||||
for (int i = 0; i < mChecks.length; i++) {
|
||||
if (mChecks[i]) {
|
||||
filter.add(gnssTypes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
PreferenceUtils.saveGnssFilter(Application.Companion.getApp(), filter, Application.Companion.getPrefs());
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface arg0, int which, boolean isChecked) {
|
||||
mChecks[which] = isChecked;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.gpstest;
|
||||
package com.android.gpstest.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
@@ -24,7 +24,10 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.android.gpstest.util.IOUtils;
|
||||
import com.android.gpstest.Application;
|
||||
import com.android.gpstest.BuildConfig;
|
||||
import com.android.gpstest.R;
|
||||
import com.android.gpstest.library.util.IOUtils;
|
||||
|
||||
public class HelpActivity extends AppCompatActivity {
|
||||
|
||||
@@ -57,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");
|
||||
@@ -73,6 +76,6 @@ public class HelpActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
// For dynamically changing the locale
|
||||
super.attachBaseContext(Application.getLocaleManager().setLocale(base));
|
||||
super.attachBaseContext(Application.Companion.getLocaleManager().setLocale(base));
|
||||
}
|
||||
}
|
||||
932
GPSTest/src/main/java/com/android/gpstest/ui/MainActivity.kt
Normal file
@@ -0,0 +1,932 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
package com.android.gpstest.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.CheckBox
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.core.view.MenuItemCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
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.BuildConfig
|
||||
import com.android.gpstest.ForegroundOnlyLocationService
|
||||
import com.android.gpstest.ForegroundOnlyLocationService.LocalBinder
|
||||
import com.android.gpstest.R
|
||||
import com.android.gpstest.databinding.ActivityMainBinding
|
||||
import com.android.gpstest.library.data.FixState
|
||||
import com.android.gpstest.library.data.LocationRepository
|
||||
import com.android.gpstest.library.ui.SignalInfoViewModel
|
||||
import com.android.gpstest.library.util.*
|
||||
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.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.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
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
|
||||
private var useDarkTheme = false
|
||||
|
||||
/**
|
||||
* Currently selected navigation drawer position (so we don't unnecessarily swap fragments
|
||||
* if the same item is selected). Initialized to -1 so the initial callback from
|
||||
* NavigationDrawerFragment always instantiates the fragments
|
||||
*/
|
||||
private var currentNavDrawerPosition = -1
|
||||
|
||||
//
|
||||
// Fragments controlled by the nav drawer
|
||||
//
|
||||
private var statusFragment: StatusFragment? = null
|
||||
private var mapFragment: MapFragment? = null
|
||||
private var skyFragment: SkyFragment? = null
|
||||
private var accuracyFragment: MapFragment? = null
|
||||
|
||||
// Main signal view model
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val signalInfoViewModel: SignalInfoViewModel by viewModels()
|
||||
|
||||
private var switch: SwitchMaterial? = null
|
||||
private var lastLocation: Location? = null
|
||||
var lastSavedInstanceState: Bundle? = null
|
||||
private var userDeniedPermission = false
|
||||
private var benchmarkController: BenchmarkController? = null
|
||||
|
||||
private var initialLanguage: String? = null
|
||||
private var initialMinTimeMillis: Long? = null
|
||||
private var initialMinDistance: Float? = null
|
||||
|
||||
private var shareDialogOpen = false
|
||||
private var progressBar: ProgressBar? = null
|
||||
private var isServiceBound = false
|
||||
private var service: ForegroundOnlyLocationService? = null
|
||||
|
||||
private var foregroundOnlyServiceConnection: ServiceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(componentName: ComponentName, iBinder: IBinder) {
|
||||
val binder = iBinder as LocalBinder
|
||||
service = binder.service
|
||||
isServiceBound = true
|
||||
if (locationFlow?.isActive == true) {
|
||||
// Activity started location updates but service wasn't bound yet - tell service to start now
|
||||
service?.subscribeToLocationUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(componentName: ComponentName) {
|
||||
service = null
|
||||
isServiceBound = false
|
||||
}
|
||||
}
|
||||
|
||||
// Repository of location data that the service will observe, injected via Hilt
|
||||
@Inject
|
||||
lateinit var repository: LocationRepository
|
||||
|
||||
// Get a reference to the Job from the Flow so we can stop it from UI events
|
||||
private var locationFlow: Job? = null
|
||||
|
||||
// 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() }, prefs)
|
||||
|
||||
/** Called when the activity is first created. */
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Set theme
|
||||
if (darkTheme(app, prefs)) {
|
||||
setTheme(R.style.AppTheme_Dark_NoActionBar)
|
||||
useDarkTheme = true
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
// Reset the activity title to make sure dynamic locale changes are shown
|
||||
LibUIUtils.resetActivityTitle(this)
|
||||
saveInstanceState(savedInstanceState)
|
||||
|
||||
// Observe stopping location updates from the service
|
||||
prefs.registerOnSharedPreferenceChangeListener(stopTrackingListener)
|
||||
|
||||
// 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), prefs)
|
||||
initialMinTimeMillis = minTimeMillis(app, prefs)
|
||||
initialMinDistance = minDistance(app, prefs)
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
|
||||
benchmarkController = BenchmarkControllerImpl(this, findViewById(R.id.mainlayout))
|
||||
|
||||
// Set initial Benchmark view visibility here - we can't do it before setContentView() b/c views aren't inflated yet
|
||||
if (accuracyFragment != null && currentNavDrawerPosition == NavigationDrawerFragment.NAVDRAWER_ITEM_ACCURACY) {
|
||||
initAccuracy()
|
||||
} else {
|
||||
(benchmarkController as BenchmarkControllerImpl).hide()
|
||||
}
|
||||
setSupportActionBar(binding.toolbar)
|
||||
progressBar = findViewById(R.id.progress_horizontal)
|
||||
setupNavigationDrawer()
|
||||
val serviceIntent = Intent(this, ForegroundOnlyLocationService::class.java)
|
||||
bindService(serviceIntent, foregroundOnlyServiceConnection, BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
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(app, intent) || IOUtils.isGeoIntent(app, intent)) {
|
||||
recreateApp(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save instance state locally so we can use it after the permission callback
|
||||
* @param savedInstanceState instance state to save
|
||||
*/
|
||||
private fun saveInstanceState(savedInstanceState: Bundle?) {
|
||||
if (savedInstanceState != null) {
|
||||
lastSavedInstanceState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
savedInstanceState.deepCopy()
|
||||
} else {
|
||||
savedInstanceState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupNavigationDrawer() {
|
||||
// Fragment managing the behaviors, interactions and presentation of the navigation drawer.
|
||||
val navDrawerFragment =
|
||||
supportFragmentManager.findFragmentById(R.id.navigation_drawer) as NavigationDrawerFragment?
|
||||
|
||||
// Set up the drawer.
|
||||
navDrawerFragment!!.setUp(
|
||||
R.id.navigation_drawer,
|
||||
binding.navDrawerLeftPane
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
shareDialogOpen = false
|
||||
if (!userDeniedPermission) {
|
||||
requestPermissionAndInit(this)
|
||||
} 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
|
||||
LibUIUtils.showLocationPermissionDialog(this)
|
||||
}
|
||||
maybeRecreateApp()
|
||||
benchmarkController!!.onResume()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
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) {
|
||||
Log.i(TAG, "Uri: $uri")
|
||||
val location = lastLocation
|
||||
shareDialogOpen = true
|
||||
UIUtils.showShareFragmentDialog(
|
||||
this, location, isFileLoggingEnabled(app, prefs),
|
||||
service!!.csvFileLogger, service!!.jsonFileLogger, uri
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// See if this result was a scanned QR Code with a ground truth location
|
||||
val scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
|
||||
if (scanResult != null) {
|
||||
val geoUri = scanResult.contents
|
||||
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(app, l)
|
||||
recreateApp(showRadar)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this, getString(R.string.qr_code_cannot_read_code),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeRecreateApp() {
|
||||
// If the set language has changed since we created the Activity (e.g., returning from Settings), recreate App
|
||||
if (prefs.contains(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(app, prefs) != initialMinTimeMillis || minDistance(app, prefs) != initialMinDistance) {
|
||||
initialMinTimeMillis = minTimeMillis(app, prefs)
|
||||
initialMinDistance = minDistance(app, prefs)
|
||||
recreateApp(null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys and recreates the main activity in a new process. If we don't use a new process,
|
||||
* the map state and Accuracy ground truth location TextViews get messed up with mixed locales
|
||||
* and partial state retention.
|
||||
* @param currentIntent the Intent to pass to the re-created app, or null if there is no intent to pass
|
||||
*/
|
||||
private fun recreateApp(currentIntent: Intent?) {
|
||||
val i = Intent(this, MainActivity::class.java)
|
||||
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(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(app, l)
|
||||
i.action = showRadarIntent.action
|
||||
i.putExtras(showRadarIntent.extras!!)
|
||||
}
|
||||
}
|
||||
startActivity(i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
// Restart process to destroy and recreate everything
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
// For dynamically changing the locale
|
||||
super.attachBaseContext(Application.localeManager.setLocale(base))
|
||||
}
|
||||
|
||||
private fun initAccuracy() {
|
||||
accuracyFragment!!.setOnMapClickListener { location: Location? ->
|
||||
benchmarkController!!.onMapClick(
|
||||
location
|
||||
)
|
||||
}
|
||||
benchmarkController!!.show()
|
||||
}
|
||||
|
||||
private fun requestPermissionAndInit(activity: Activity) {
|
||||
if (PermissionUtils.hasGrantedPermissions(activity, PermissionUtils.REQUIRED_PERMISSIONS)) {
|
||||
initGnss()
|
||||
} else {
|
||||
// Request permissions from the user
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
PermissionUtils.REQUIRED_PERMISSIONS,
|
||||
PermissionUtils.LOCATION_PERMISSION_REQUEST
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int, permissions: Array<String>, grantResults: IntArray,
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode == PermissionUtils.LOCATION_PERMISSION_REQUEST) {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
userDeniedPermission = false
|
||||
initGnss()
|
||||
} else {
|
||||
userDeniedPermission = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initGnss() {
|
||||
val locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
|
||||
val provider = locationManager.getProvider(LocationManager.GPS_PROVIDER)
|
||||
if (provider == null) {
|
||||
Log.e(TAG, "Unable to get GPS_PROVIDER")
|
||||
Toast.makeText(
|
||||
this, getString(R.string.gps_not_supported),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
|
||||
LibUIUtils.promptEnableGps(app,this)
|
||||
}
|
||||
setupStartState(lastSavedInstanceState)
|
||||
|
||||
// If the theme has changed (e.g., from Preferences), destroy and recreate to reflect change
|
||||
val useDarkTheme = darkTheme(app, prefs)
|
||||
if (this.useDarkTheme != useDarkTheme) {
|
||||
this.useDarkTheme = useDarkTheme
|
||||
recreate()
|
||||
}
|
||||
val settings = prefs
|
||||
checkKeepScreenOn(settings)
|
||||
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(app, prefs)) {
|
||||
service?.unsubscribeToLocationUpdates()
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun setupStartState(savedInstanceState: Bundle?) {
|
||||
// Use "Auto-start GNSS" setting, or existing tracking state (e.g., if service is running)
|
||||
if (prefs.getBoolean(
|
||||
getString(R.string.pref_key_auto_start_gps),
|
||||
true
|
||||
) || isTrackingStarted(prefs)
|
||||
) {
|
||||
gpsStart()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNavigationDrawerItemSelected(position: Int) {
|
||||
goToNavDrawerItem(position)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun goToNavDrawerItem(item: Int) {
|
||||
// Update the main content by replacing fragments
|
||||
when (item) {
|
||||
NavigationDrawerFragment.NAVDRAWER_ITEM_STATUS -> if (currentNavDrawerPosition != NavigationDrawerFragment.NAVDRAWER_ITEM_STATUS) {
|
||||
showStatusFragment()
|
||||
currentNavDrawerPosition = item
|
||||
}
|
||||
NavigationDrawerFragment.NAVDRAWER_ITEM_MAP -> if (currentNavDrawerPosition != NavigationDrawerFragment.NAVDRAWER_ITEM_MAP) {
|
||||
showMapFragment()
|
||||
currentNavDrawerPosition = item
|
||||
}
|
||||
NavigationDrawerFragment.NAVDRAWER_ITEM_SKY -> if (currentNavDrawerPosition != NavigationDrawerFragment.NAVDRAWER_ITEM_SKY) {
|
||||
showSkyFragment()
|
||||
currentNavDrawerPosition = item
|
||||
}
|
||||
NavigationDrawerFragment.NAVDRAWER_ITEM_ACCURACY -> if (currentNavDrawerPosition != NavigationDrawerFragment.NAVDRAWER_ITEM_ACCURACY) {
|
||||
showAccuracyFragment()
|
||||
currentNavDrawerPosition = item
|
||||
}
|
||||
NavigationDrawerFragment.NAVDRAWER_ITEM_INJECT_PSDS_DATA -> forcePsdsInjection()
|
||||
NavigationDrawerFragment.NAVDRAWER_ITEM_INJECT_TIME_DATA -> forceTimeInjection()
|
||||
NavigationDrawerFragment.NAVDRAWER_ITEM_CLEAR_AIDING_DATA -> {
|
||||
val prefs = prefs
|
||||
if (!prefs.getBoolean(
|
||||
getString(R.string.pref_key_never_show_clear_assist_warning),
|
||||
false
|
||||
)
|
||||
) {
|
||||
showDialog(LibUIUtils.CLEAR_ASSIST_WARNING_DIALOG)
|
||||
} else {
|
||||
deleteAidingData()
|
||||
}
|
||||
}
|
||||
NavigationDrawerFragment.NAVDRAWER_ITEM_SETTINGS -> startActivity(
|
||||
Intent(
|
||||
this,
|
||||
Preferences::class.java
|
||||
)
|
||||
)
|
||||
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))
|
||||
startActivity(i)
|
||||
}
|
||||
NavigationDrawerFragment.NAVDRAWER_ITEM_SEND_FEEDBACK -> {
|
||||
// Send App feedback
|
||||
val email = getString(R.string.app_feedback_email)
|
||||
var locationString: String? = null
|
||||
if (lastLocation != null) {
|
||||
locationString = LocationUtils.printLocationDetails(lastLocation)
|
||||
}
|
||||
LibUIUtils.sendEmail(this, email, locationString, signalInfoViewModel, BuildUtils.getPlayServicesVersion(), prefs, BuildConfig.FLAVOR)
|
||||
}
|
||||
}
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
private fun showStatusFragment() {
|
||||
val fm = supportFragmentManager
|
||||
// Hide everything that shouldn't be shown
|
||||
hideMapFragment()
|
||||
hideSkyFragment()
|
||||
hideAccuracyFragment()
|
||||
if (benchmarkController != null) {
|
||||
benchmarkController!!.hide()
|
||||
}
|
||||
|
||||
// Show fragment (we use show instead of replace to keep the map state)
|
||||
if (statusFragment == null) {
|
||||
// First check to see if an instance of fragment already exists
|
||||
statusFragment = fm.findFragmentByTag(TAG) as StatusFragment?
|
||||
if (statusFragment == null) {
|
||||
// No existing fragment was found, so create a new one
|
||||
Log.d(TAG, "Creating new StatusFragment")
|
||||
statusFragment = StatusFragment()
|
||||
fm.beginTransaction()
|
||||
.add(R.id.fragment_container, statusFragment!!, TAG)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
supportFragmentManager.beginTransaction().show(statusFragment!!).commit()
|
||||
title = resources.getString(R.string.gps_status_title)
|
||||
}
|
||||
|
||||
private fun hideStatusFragment() {
|
||||
val fm = supportFragmentManager
|
||||
statusFragment = fm.findFragmentByTag(TAG) as StatusFragment?
|
||||
if (statusFragment != null && !statusFragment!!.isHidden) {
|
||||
fm.beginTransaction().hide(statusFragment!!).commit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMapFragment() {
|
||||
val fm = supportFragmentManager
|
||||
// Hide everything that shouldn't be shown
|
||||
hideStatusFragment()
|
||||
hideSkyFragment()
|
||||
hideAccuracyFragment()
|
||||
if (benchmarkController != null) {
|
||||
benchmarkController!!.hide()
|
||||
}
|
||||
|
||||
// Show fragment (we use show instead of replace to keep the map state)
|
||||
if (mapFragment == null) {
|
||||
// First check to see if an instance of fragment already exists
|
||||
mapFragment = fm.findFragmentByTag(MapConstants.MODE_MAP) as MapFragment?
|
||||
if (mapFragment == null) {
|
||||
// No existing fragment was found, so create a new one
|
||||
Log.d(TAG, "Creating new MapFragment")
|
||||
val bundle = Bundle()
|
||||
bundle.putString(MapConstants.MODE, MapConstants.MODE_MAP)
|
||||
mapFragment = MapFragment()
|
||||
mapFragment!!.arguments = bundle
|
||||
fm.beginTransaction()
|
||||
.add(R.id.fragment_container, mapFragment!!, MapConstants.MODE_MAP)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
supportFragmentManager.beginTransaction().show(mapFragment!!).commit()
|
||||
title = resources.getString(R.string.gps_map_title)
|
||||
}
|
||||
|
||||
private fun hideMapFragment() {
|
||||
val fm = supportFragmentManager
|
||||
mapFragment = fm.findFragmentByTag(MapConstants.MODE_MAP) as MapFragment?
|
||||
if (mapFragment != null && !mapFragment!!.isHidden) {
|
||||
fm.beginTransaction().hide(mapFragment!!).commit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSkyFragment() {
|
||||
val fm = supportFragmentManager
|
||||
// Hide everything that shouldn't be shown
|
||||
hideStatusFragment()
|
||||
hideMapFragment()
|
||||
hideAccuracyFragment()
|
||||
if (benchmarkController != null) {
|
||||
benchmarkController!!.hide()
|
||||
}
|
||||
// Show fragment (we use show instead of replace to keep the map state)
|
||||
if (skyFragment == null) {
|
||||
// First check to see if an instance of fragment already exists
|
||||
skyFragment = fm.findFragmentByTag(SkyFragment.TAG) as SkyFragment?
|
||||
if (skyFragment == null) {
|
||||
// No existing fragment was found, so create a new one
|
||||
Log.d(TAG, "Creating new SkyFragment")
|
||||
skyFragment = SkyFragment()
|
||||
fm.beginTransaction()
|
||||
.add(R.id.fragment_container, skyFragment!!, SkyFragment.TAG)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
supportFragmentManager.beginTransaction().show(skyFragment!!).commit()
|
||||
title = resources.getString(R.string.gps_sky_title)
|
||||
}
|
||||
|
||||
private fun hideSkyFragment() {
|
||||
val fm = supportFragmentManager
|
||||
skyFragment = fm.findFragmentByTag(SkyFragment.TAG) as SkyFragment?
|
||||
if (skyFragment != null && !skyFragment!!.isHidden) {
|
||||
fm.beginTransaction().hide(skyFragment!!).commit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAccuracyFragment() {
|
||||
val fm = supportFragmentManager
|
||||
// Hide everything that shouldn't be shown
|
||||
hideStatusFragment()
|
||||
hideMapFragment()
|
||||
hideSkyFragment()
|
||||
// Show fragment (we use show instead of replace to keep the map state)
|
||||
if (accuracyFragment == null) {
|
||||
// First check to see if an instance of fragment already exists
|
||||
accuracyFragment = fm.findFragmentByTag(MapConstants.MODE_ACCURACY) as MapFragment?
|
||||
if (accuracyFragment == null) {
|
||||
// No existing fragment was found, so create a new one
|
||||
Log.d(TAG, "Creating new AccuracyFragment for Accuracy")
|
||||
val bundle = Bundle()
|
||||
bundle.putString(MapConstants.MODE, MapConstants.MODE_ACCURACY)
|
||||
accuracyFragment = MapFragment()
|
||||
accuracyFragment!!.arguments = bundle
|
||||
fm.beginTransaction()
|
||||
.add(R.id.fragment_container, accuracyFragment!!, MapConstants.MODE_ACCURACY)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
supportFragmentManager.beginTransaction().show(accuracyFragment!!).commit()
|
||||
title = resources.getString(R.string.gps_accuracy_title)
|
||||
if (benchmarkController != null) {
|
||||
initAccuracy()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideAccuracyFragment() {
|
||||
val fm = supportFragmentManager
|
||||
accuracyFragment = fm.findFragmentByTag(MapConstants.MODE_ACCURACY) as MapFragment?
|
||||
if (accuracyFragment != null && !accuracyFragment!!.isHidden) {
|
||||
fm.beginTransaction().hide(accuracyFragment!!).commit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun forcePsdsInjection() {
|
||||
val success =
|
||||
IOUtils.forcePsdsInjection(app, getSystemService(LOCATION_SERVICE) as LocationManager)
|
||||
if (success) {
|
||||
Toast.makeText(
|
||||
this, getString(R.string.force_psds_injection_success),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
PreferenceUtils.saveInt(
|
||||
app.getString(R.string.capability_key_inject_psds),
|
||||
PreferenceUtils.CAPABILITY_SUPPORTED,
|
||||
prefs
|
||||
)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this, getString(R.string.force_psds_injection_failure),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
PreferenceUtils.saveInt(
|
||||
app.getString(R.string.capability_key_inject_psds),
|
||||
PreferenceUtils.CAPABILITY_NOT_SUPPORTED,
|
||||
prefs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun forceTimeInjection() {
|
||||
val success =
|
||||
IOUtils.forceTimeInjection(app, getSystemService(LOCATION_SERVICE) as LocationManager)
|
||||
if (success) {
|
||||
Toast.makeText(
|
||||
this, getString(R.string.force_time_injection_success),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
PreferenceUtils.saveInt(
|
||||
app.getString(R.string.capability_key_inject_time),
|
||||
PreferenceUtils.CAPABILITY_SUPPORTED,
|
||||
prefs
|
||||
)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this, getString(R.string.force_time_injection_failure),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
PreferenceUtils.saveInt(
|
||||
app.getString(R.string.capability_key_inject_time),
|
||||
PreferenceUtils.CAPABILITY_NOT_SUPPORTED,
|
||||
prefs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun deleteAidingData() {
|
||||
// If GPS is currently running, stop it
|
||||
val lastStartState = isTrackingStarted(prefs)
|
||||
if (isTrackingStarted(prefs)) {
|
||||
gpsStop()
|
||||
}
|
||||
val success =
|
||||
IOUtils.deleteAidingData(app, getSystemService(LOCATION_SERVICE) as LocationManager)
|
||||
if (success) {
|
||||
Toast.makeText(
|
||||
this, getString(R.string.delete_aiding_data_success),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
PreferenceUtils.saveInt(
|
||||
app.getString(R.string.capability_key_delete_assist),
|
||||
PreferenceUtils.CAPABILITY_SUPPORTED,
|
||||
prefs
|
||||
)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this, getString(R.string.delete_aiding_data_failure),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
PreferenceUtils.saveInt(
|
||||
app.getString(R.string.capability_key_delete_assist),
|
||||
PreferenceUtils.CAPABILITY_NOT_SUPPORTED,
|
||||
prefs
|
||||
)
|
||||
}
|
||||
// Restart the GPS, if it was previously started, with a slight delay,
|
||||
// to refresh the assistance data
|
||||
if (lastStartState) {
|
||||
lifecycleScope.launch {
|
||||
delay(500)
|
||||
gpsStart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (binding.navDrawerLeftPane.isDrawerOpen(GravityCompat.START)) {
|
||||
// Close navigation drawer
|
||||
binding.navDrawerLeftPane.closeDrawer(GravityCompat.START)
|
||||
return
|
||||
} else if (benchmarkController != null) {
|
||||
// Close sliding drawer
|
||||
if (benchmarkController!!.onBackPressed()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
@SuppressLint("MissingPermission")
|
||||
@Synchronized
|
||||
private fun gpsStart() {
|
||||
PreferenceUtils.saveTrackingStarted(true, prefs)
|
||||
service?.subscribeToLocationUpdates()
|
||||
showProgressBar()
|
||||
|
||||
// Observe flows
|
||||
observeLocationFlow()
|
||||
observeGnssStates()
|
||||
|
||||
// Show Toast only if the user has set minTime or minDistance to something other than default values
|
||||
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(app, prefs).toDouble() / SECONDS_TO_MILLISECONDS).toString(),
|
||||
minDistance(app, prefs).toString()
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
// Reset the options menu to trigger updates to action bar menu items
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeLocationFlow() {
|
||||
// This should be a Flow and not LiveData to ensure that the Flow is active before the Service is bound
|
||||
if (locationFlow?.isActive == true) {
|
||||
// If we're already observing updates, don't register again
|
||||
return
|
||||
}
|
||||
// Observe locations via Flow as they are generated by the repository
|
||||
locationFlow = repository.getLocations()
|
||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
||||
.onEach {
|
||||
lastLocation = it
|
||||
//Log.d(TAG, "Activity location: ${it.toNotificationTitle()}")
|
||||
|
||||
hideProgressBar()
|
||||
|
||||
// Reset the options menu to trigger updates to action bar menu items
|
||||
invalidateOptionsMenu()
|
||||
|
||||
benchmarkController?.onLocationChanged(it)
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun observeGnssStates() {
|
||||
// Use ViewModel here to ensure that it's populated for fragments as well -
|
||||
// otherwise ViewModel is lazily initialized and we don't save TTFF if viewed later in Status (e.g., if started in Accuracy or Map)
|
||||
val gnssStateObserver = Observer<FixState> { fixState ->
|
||||
when (fixState) {
|
||||
is FixState.Acquired -> hideProgressBar()
|
||||
is FixState.NotAcquired -> if (isTrackingStarted(prefs)) showProgressBar()
|
||||
}
|
||||
}
|
||||
signalInfoViewModel.fixState.observe(
|
||||
this, gnssStateObserver
|
||||
)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun gpsStop() {
|
||||
PreferenceUtils.saveTrackingStarted(false, prefs)
|
||||
locationFlow?.cancel()
|
||||
|
||||
// Reset the options menu to trigger updates to action bar menu items
|
||||
invalidateOptionsMenu()
|
||||
progressBar?.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun hideProgressBar() {
|
||||
val p = progressBar
|
||||
if (p != null) {
|
||||
LibUIUtils.hideViewWithAnimation(p, LibUIUtils.ANIMATION_DURATION_SHORT_MS)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showProgressBar() {
|
||||
val p = progressBar
|
||||
if (p != null) {
|
||||
LibUIUtils.showViewWithAnimation(p, LibUIUtils.ANIMATION_DURATION_SHORT_MS)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkKeepScreenOn(settings: SharedPreferences) {
|
||||
binding.toolbar.keepScreenOn =
|
||||
settings.getBoolean(getString(R.string.pref_key_keep_screen_on), true)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.main_menu, menu)
|
||||
initGpsSwitch(menu)
|
||||
return true
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun initGpsSwitch(menu: Menu) {
|
||||
val item = menu.findItem(R.id.gps_switch_item)
|
||||
if (item != null) {
|
||||
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(prefs)
|
||||
|
||||
// Set up listener for GPS on/off switch
|
||||
switch!!.setOnClickListener {
|
||||
// Turn GPS on or off
|
||||
if (!switch!!.isChecked && isTrackingStarted(prefs)) {
|
||||
gpsStop()
|
||||
service?.unsubscribeToLocationUpdates()
|
||||
} else {
|
||||
if (switch!!.isChecked && !isTrackingStarted(prefs)) {
|
||||
gpsStart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
val item = menu.findItem(R.id.share)
|
||||
if (item != null) {
|
||||
item.isVisible = lastLocation != null || isFileLoggingEnabled(app, prefs) == true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
// Handle menu item selection
|
||||
when (item.itemId) {
|
||||
R.id.gps_switch -> {
|
||||
return true
|
||||
}
|
||||
R.id.share -> {
|
||||
share()
|
||||
return true
|
||||
}
|
||||
R.id.filter_sats -> {
|
||||
UIUtils.showFilterDialog(this)
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun share() {
|
||||
val location = lastLocation
|
||||
shareDialogOpen = true
|
||||
UIUtils.showShareFragmentDialog(
|
||||
this, location, isFileLoggingEnabled(app, prefs),
|
||||
service!!.csvFileLogger, service!!.jsonFileLogger, null
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(id: Int): Dialog {
|
||||
when (id) {
|
||||
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)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun createClearAssistWarningDialog(): Dialog {
|
||||
val view = layoutInflater.inflate(R.layout.clear_assist_warning, null)
|
||||
val neverShowDialog = view.findViewById<CheckBox>(R.id.clear_assist_never_ask_again)
|
||||
neverShowDialog.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
||||
// Save the preference
|
||||
PreferenceUtils.saveBoolean(
|
||||
getString(R.string.pref_key_never_show_clear_assist_warning),
|
||||
isChecked,
|
||||
prefs
|
||||
)
|
||||
}
|
||||
val icon = ContextCompat.getDrawable(app, R.drawable.ic_delete)
|
||||
if (icon != null) {
|
||||
DrawableCompat.setTint(icon, resources.getColor(R.color.colorPrimary))
|
||||
}
|
||||
val builder = AlertDialog.Builder(this)
|
||||
.setTitle(R.string.clear_assist_warning_title)
|
||||
.setIcon(icon)
|
||||
.setCancelable(false)
|
||||
.setView(view)
|
||||
.setPositiveButton(
|
||||
R.string.yes
|
||||
) { _: DialogInterface?, _: Int -> deleteAidingData() }
|
||||
.setNegativeButton(
|
||||
R.string.no
|
||||
) { _: DialogInterface?, _: Int -> }
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "GpsTestActivity"
|
||||
private const val SECONDS_TO_MILLISECONDS = 1000
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
* and a generated NavigationDrawer app from Android Studio, modified for OneBusAway by USF,
|
||||
* modified for GPSTest by Sean J. Barbeau
|
||||
*/
|
||||
package com.android.gpstest;
|
||||
package com.android.gpstest.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
@@ -42,8 +42,10 @@ import androidx.core.view.GravityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.android.gpstest.util.IOUtils;
|
||||
import com.android.gpstest.util.UIUtils;
|
||||
import com.android.gpstest.Application;
|
||||
import com.android.gpstest.R;
|
||||
import com.android.gpstest.library.util.IOUtils;
|
||||
import com.android.gpstest.library.util.LibUIUtils;
|
||||
import com.android.gpstest.view.ScrimInsetsScrollView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -173,9 +175,9 @@ public class NavigationDrawerFragment extends Fragment {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
SharedPreferences sp = Application.getPrefs();
|
||||
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);
|
||||
@@ -304,7 +306,7 @@ public class NavigationDrawerFragment extends Fragment {
|
||||
* Set the selected position as a preference
|
||||
*/
|
||||
public void setSavedPosition(int position) {
|
||||
SharedPreferences sp = Application.getPrefs();
|
||||
SharedPreferences sp = Application.Companion.getPrefs();
|
||||
sp.edit().putInt(STATE_SELECTED_POSITION, position).apply();
|
||||
}
|
||||
|
||||
@@ -429,7 +431,7 @@ public class NavigationDrawerFragment extends Fragment {
|
||||
}
|
||||
|
||||
// Set background color of nav drawer
|
||||
if (Application.getPrefs().getBoolean(getString(R.string.pref_key_dark_theme), false)) {
|
||||
if (Application.Companion.getPrefs().getBoolean(getString(R.string.pref_key_dark_theme), false)) {
|
||||
mDrawerItemsListContainer.setBackgroundColor(getContext().getResources().getColor(R.color.navdrawer_background_dark));
|
||||
}
|
||||
|
||||
@@ -461,11 +463,11 @@ public class NavigationDrawerFragment extends Fragment {
|
||||
|
||||
if (isSeparator(itemId)) {
|
||||
// we are done
|
||||
UIUtils.setAccessibilityIgnore(view);
|
||||
LibUIUtils.setAccessibilityIgnore(view);
|
||||
return view;
|
||||
}
|
||||
|
||||
if (Application.getPrefs().getBoolean(getString(R.string.pref_key_dark_theme), false)
|
||||
if (Application.Companion.getPrefs().getBoolean(getString(R.string.pref_key_dark_theme), false)
|
||||
&& layoutToInflate == R.layout.navdrawer_item) {
|
||||
// Dark theme
|
||||
view.setBackgroundResource(R.drawable.navdrawer_item_selectable_dark);
|
||||
@@ -538,7 +540,7 @@ public class NavigationDrawerFragment extends Fragment {
|
||||
// Show the category as not highlighted, if its not currently selected
|
||||
if (itemId != mCurrentSelectedPosition) {
|
||||
view.setSelected(false);
|
||||
if (Application.getPrefs().getBoolean(getString(R.string.pref_key_dark_theme), false)) {
|
||||
if (Application.Companion.getPrefs().getBoolean(getString(R.string.pref_key_dark_theme), false)) {
|
||||
// Dark theme
|
||||
titleView.setTextColor(getResources().getColor(R.color.navdrawer_text_color_dark));
|
||||
iconView.setColorFilter(getResources().getColor(R.color.navdrawer_icon_tint_dark));
|
||||
498
GPSTest/src/main/java/com/android/gpstest/ui/Preferences.kt
Normal file
@@ -0,0 +1,498 @@
|
||||
/*
|
||||
* Copyright (C) 2013-2021 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.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.LocationManager
|
||||
import android.os.Build.VERSION
|
||||
import android.os.Build.VERSION_CODES
|
||||
import android.os.Bundle
|
||||
import android.preference.CheckBoxPreference
|
||||
import android.preference.EditTextPreference
|
||||
import android.preference.ListPreference
|
||||
import android.preference.Preference
|
||||
import android.preference.Preference.OnPreferenceChangeListener
|
||||
import android.preference.PreferenceActivity
|
||||
import android.preference.PreferenceCategory
|
||||
import android.text.InputType
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.app.ActivityCompat
|
||||
import com.android.gpstest.Application.Companion.app
|
||||
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.library.util.LibUIUtils.resetActivityTitle
|
||||
import com.android.gpstest.library.util.PermissionUtils
|
||||
import com.android.gpstest.library.util.PreferenceUtil
|
||||
import com.android.gpstest.library.util.PreferenceUtil.enableMeasurementsPref
|
||||
import com.android.gpstest.library.util.PreferenceUtil.enableNavMessagesPref
|
||||
import com.android.gpstest.library.util.PreferenceUtils
|
||||
import com.android.gpstest.library.util.SatelliteUtils
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class Preferences : PreferenceActivity(), OnSharedPreferenceChangeListener {
|
||||
var forceFullGnssMeasurements: CheckBoxPreference? = null
|
||||
var txtMinTime: EditTextPreference? = null
|
||||
var txtMinDistance: EditTextPreference? = null
|
||||
var chkDarkTheme: CheckBoxPreference? = null
|
||||
|
||||
private var actionBar: Toolbar? = null
|
||||
|
||||
var preferredDistanceUnits: ListPreference? = null
|
||||
var preferredSpeedUnits: ListPreference? = null
|
||||
|
||||
var language: ListPreference? = null
|
||||
|
||||
var chkShowNotification: CheckBoxPreference? = null
|
||||
var chkRunInBackground: CheckBoxPreference? = null
|
||||
var chkLogFileNmea: CheckBoxPreference? = null
|
||||
var chkLogFileNavMessages: CheckBoxPreference? = null
|
||||
var chkLogFileMeasurements: CheckBoxPreference? = null
|
||||
var chkLogFileLocation: CheckBoxPreference? = null
|
||||
var chkLogFileAntennaJson: CheckBoxPreference? = null
|
||||
var chkLogFileAntennaCsv: CheckBoxPreference? = null
|
||||
|
||||
var chkAsMeasurements: CheckBoxPreference? = null
|
||||
var chkAsNavMessages: CheckBoxPreference? = null
|
||||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Set theme
|
||||
if (prefs.getBoolean(getString(R.string.pref_key_dark_theme), false)) {
|
||||
setTheme(R.style.AppTheme_Dark)
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
addPreferencesFromResource(R.xml.preferences)
|
||||
actionBar?.title = title
|
||||
resetActivityTitle(this)
|
||||
forceFullGnssMeasurements =
|
||||
findPreference(getString(R.string.pref_key_force_full_gnss_measurements)) as CheckBoxPreference
|
||||
if (!SatelliteUtils.isForceFullGnssMeasurementsSupported()) {
|
||||
forceFullGnssMeasurements!!.isEnabled = false
|
||||
}
|
||||
txtMinTime = findPreference(getString(R.string.pref_key_gps_min_time)) as EditTextPreference
|
||||
txtMinTime?.editText?.inputType =
|
||||
InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
|
||||
|
||||
// Verify minTime entry
|
||||
txtMinTime?.onPreferenceChangeListener =
|
||||
OnPreferenceChangeListener { _, newValue ->
|
||||
if (!verifyFloat(newValue)) {
|
||||
// Tell user that entry must be valid decimal
|
||||
Toast.makeText(
|
||||
this@Preferences,
|
||||
getString(R.string.pref_gps_min_time_invalid_entry),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
txtMinDistance =
|
||||
findPreference(getString(R.string.pref_key_gps_min_distance)) as EditTextPreference
|
||||
txtMinDistance?.editText?.inputType =
|
||||
InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
|
||||
|
||||
// Verify minDistance entry
|
||||
txtMinDistance?.onPreferenceChangeListener =
|
||||
OnPreferenceChangeListener { _, newValue ->
|
||||
if (!verifyFloat(newValue)) {
|
||||
// Tell user that entry must be valid decimal
|
||||
Toast.makeText(
|
||||
this@Preferences,
|
||||
getString(R.string.pref_gps_min_distance_invalid_entry),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
// Check Dark Theme
|
||||
chkDarkTheme = findPreference(getString(R.string.pref_key_dark_theme)) as CheckBoxPreference
|
||||
chkDarkTheme?.onPreferenceChangeListener =
|
||||
OnPreferenceChangeListener { _: Preference?, _: Any? ->
|
||||
// Destroy and recreate Activity
|
||||
recreate()
|
||||
true
|
||||
}
|
||||
preferredDistanceUnits = findPreference(
|
||||
getString(R.string.pref_key_preferred_distance_units_v2)
|
||||
) as ListPreference
|
||||
preferredSpeedUnits = findPreference(
|
||||
getString(R.string.pref_key_preferred_speed_units_v2)
|
||||
) as ListPreference
|
||||
language = findPreference(getString(R.string.pref_key_language)) as ListPreference
|
||||
language?.onPreferenceChangeListener =
|
||||
OnPreferenceChangeListener { preference: Preference?, newValue: Any ->
|
||||
localeManager.setNewLocale(app, newValue.toString())
|
||||
// Destroy and recreate Activity
|
||||
recreate()
|
||||
true
|
||||
}
|
||||
|
||||
// Remove preference for rotating map if needed
|
||||
if (!SatelliteUtils.isRotationVectorSensorSupported(this) || BuildConfig.FLAVOR != "google") {
|
||||
// We don't have tilt info or it's the OSM Droid flavor, so remove this preference
|
||||
val checkBoxTiltMap = findPreference(
|
||||
getString(R.string.pref_key_tilt_map_with_sensors)
|
||||
) as CheckBoxPreference
|
||||
val mMapCategory = findPreference(
|
||||
getString(R.string.pref_key_map_category)
|
||||
) as PreferenceCategory
|
||||
mMapCategory.removePreference(checkBoxTiltMap)
|
||||
}
|
||||
|
||||
// Remove preference for setting map type if needed
|
||||
if (BuildConfig.FLAVOR != "google") {
|
||||
// We don't have tilt info or it's the OSM Droid flavor, so remove this preference
|
||||
val checkBoxMapType = findPreference(
|
||||
getString(R.string.pref_key_map_type)
|
||||
) as ListPreference
|
||||
val mMapCategory = findPreference(
|
||||
getString(R.string.pref_key_map_category)
|
||||
) as PreferenceCategory
|
||||
mMapCategory.removePreference(checkBoxMapType)
|
||||
}
|
||||
|
||||
// Disable preferences for antenna info logging if it's not supported
|
||||
val manager = getSystemService(LOCATION_SERVICE) as LocationManager
|
||||
chkLogFileAntennaJson =
|
||||
findPreference(getString(R.string.pref_key_file_antenna_output_json)) as CheckBoxPreference
|
||||
chkLogFileAntennaCsv =
|
||||
findPreference(getString(R.string.pref_key_file_antenna_output_csv)) as CheckBoxPreference
|
||||
if(!SatelliteUtils.isGnssAntennaInfoSupported(manager)) {
|
||||
chkLogFileAntennaJson!!.isEnabled = false
|
||||
chkLogFileAntennaCsv!!.isEnabled = false
|
||||
}
|
||||
|
||||
// Disable Android Studio logging if not supported by platform
|
||||
chkAsMeasurements = findPreference(getString(R.string.pref_key_as_measurement_output)) as CheckBoxPreference
|
||||
chkAsMeasurements?.isEnabled = enableMeasurementsPref(app, prefs)
|
||||
chkAsNavMessages = findPreference(getString(R.string.pref_key_as_navigation_message_output)) as CheckBoxPreference
|
||||
chkAsNavMessages?.isEnabled = enableNavMessagesPref(app, prefs)
|
||||
|
||||
initNotificationPermissionDialog()
|
||||
|
||||
prefs.registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
changePreferenceSummary(getString(R.string.pref_key_preferred_distance_units_v2))
|
||||
changePreferenceSummary(getString(R.string.pref_key_preferred_speed_units_v2))
|
||||
changePreferenceSummary(getString(R.string.pref_key_language))
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
if (key.equals(
|
||||
getString(R.string.pref_key_preferred_distance_units_v2),
|
||||
ignoreCase = true
|
||||
)
|
||||
) {
|
||||
// Change the preferred distance units description
|
||||
changePreferenceSummary(key)
|
||||
} else {
|
||||
if (key.equals(
|
||||
getString(R.string.pref_key_preferred_speed_units_v2),
|
||||
ignoreCase = true
|
||||
)
|
||||
) {
|
||||
// Change the preferred speed units description
|
||||
changePreferenceSummary(key)
|
||||
} else {
|
||||
if (key.equals(getString(R.string.pref_key_language), ignoreCase = true)) {
|
||||
// Change the preferred language description
|
||||
changePreferenceSummary(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
// For dynamically changing the locale
|
||||
super.attachBaseContext(localeManager.setLocale(base))
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the value is a valid float
|
||||
*
|
||||
* @param newValue entered value
|
||||
* @return true if its a valid float, false if its not
|
||||
*/
|
||||
private fun verifyFloat(newValue: Any): Boolean {
|
||||
return try {
|
||||
newValue.toString().toFloat()
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun setContentView(layoutResID: Int) {
|
||||
val contentView = LayoutInflater.from(this).inflate(
|
||||
R.layout.settings_activity, LinearLayout(this), false
|
||||
) as ViewGroup
|
||||
actionBar = contentView.findViewById(R.id.action_bar)
|
||||
actionBar!!.setNavigationOnClickListener { finish() }
|
||||
val contentWrapper = contentView.findViewById<ViewGroup>(R.id.content_wrapper)
|
||||
LayoutInflater.from(this).inflate(layoutResID, contentWrapper, true)
|
||||
window.setContentView(contentView)
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the summary of a preference based on a given preference key
|
||||
*
|
||||
* @param prefKey preference key that triggers a change in summary
|
||||
*/
|
||||
private fun changePreferenceSummary(prefKey: String) {
|
||||
if (prefKey.equals(
|
||||
getString(R.string.pref_key_preferred_distance_units_v2),
|
||||
ignoreCase = true
|
||||
)
|
||||
) {
|
||||
val values = app.resources.getStringArray(R.array.preferred_distance_units_values)
|
||||
val entries = app.resources.getStringArray(R.array.preferred_distance_units_entries)
|
||||
for (i in values.indices) {
|
||||
if (values[i] == preferredDistanceUnits!!.value) {
|
||||
preferredDistanceUnits!!.summary = entries[i]
|
||||
}
|
||||
}
|
||||
} else if (prefKey.equals(
|
||||
getString(R.string.pref_key_preferred_speed_units_v2),
|
||||
ignoreCase = true
|
||||
)
|
||||
) {
|
||||
val values = app.resources.getStringArray(R.array.preferred_speed_units_values)
|
||||
val entries = app.resources.getStringArray(R.array.preferred_speed_units_entries)
|
||||
for (i in values.indices) {
|
||||
if (values[i] == preferredSpeedUnits!!.value) {
|
||||
preferredSpeedUnits!!.summary = entries[i]
|
||||
}
|
||||
}
|
||||
} else if (prefKey.equals(getString(R.string.pref_key_language), ignoreCase = true)) {
|
||||
val values = app.resources.getStringArray(R.array.language_values)
|
||||
val entries = app.resources.getStringArray(R.array.language_entries)
|
||||
for (i in values.indices) {
|
||||
if (values[i] == language!!.value) {
|
||||
language!!.summary = entries[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the dialog for notification permissions, which is required for
|
||||
* notifications, background execution and logging.
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
private fun initNotificationPermissionDialog() {
|
||||
chkShowNotification =
|
||||
findPreference(getString(R.string.pref_key_show_notification)) as CheckBoxPreference
|
||||
if (VERSION.SDK_INT < VERSION_CODES.TIRAMISU) {
|
||||
// Notifications are always shown on Android 12 and lower
|
||||
chkShowNotification?.isEnabled = false
|
||||
PreferenceUtils.saveBoolean(
|
||||
getString(R.string.pref_key_show_notification),
|
||||
true,
|
||||
prefs
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Permissions for notifications are used in place of a user-defined setting. This workflow
|
||||
// supports users that have installed an update, who will already have permissions granted.
|
||||
// Additionally, revoking notification permissions seems to be the only way to disable
|
||||
// user-facing notifications for the foreground service, because they are required.
|
||||
PreferenceUtils.saveBoolean(
|
||||
getString(R.string.pref_key_show_notification),
|
||||
PermissionUtils.hasGrantedNotificationPermissions(this),
|
||||
prefs
|
||||
)
|
||||
|
||||
chkRunInBackground =
|
||||
findPreference(getString(R.string.pref_key_gnss_background)) as CheckBoxPreference
|
||||
chkLogFileNmea =
|
||||
findPreference(getString(R.string.pref_key_file_nmea_output)) as CheckBoxPreference
|
||||
chkLogFileNavMessages =
|
||||
findPreference(getString(R.string.pref_key_file_navigation_message_output)) as CheckBoxPreference
|
||||
chkLogFileMeasurements =
|
||||
findPreference(getString(R.string.pref_key_file_measurement_output)) as CheckBoxPreference
|
||||
chkLogFileLocation =
|
||||
findPreference(getString(R.string.pref_key_file_location_output)) as CheckBoxPreference
|
||||
chkLogFileAntennaJson =
|
||||
findPreference(getString(R.string.pref_key_file_antenna_output_json)) as CheckBoxPreference
|
||||
chkLogFileAntennaCsv =
|
||||
findPreference(getString(R.string.pref_key_file_antenna_output_csv)) as CheckBoxPreference
|
||||
val prefsThatNeedNotificationPermissions = listOf(
|
||||
chkShowNotification,
|
||||
chkRunInBackground,
|
||||
chkLogFileNmea,
|
||||
chkLogFileNavMessages,
|
||||
chkLogFileMeasurements,
|
||||
chkLogFileLocation,
|
||||
chkLogFileAntennaJson,
|
||||
chkLogFileAntennaCsv
|
||||
)
|
||||
prefsThatNeedNotificationPermissions.forEach {
|
||||
it?.onPreferenceChangeListener =
|
||||
OnPreferenceChangeListener { preference, newValue ->
|
||||
if (newValue as Boolean && !PermissionUtils.hasGrantedNotificationPermissions(
|
||||
this
|
||||
)
|
||||
) {
|
||||
// User must have granted notification permissions first
|
||||
createNotificationPermissionDialog(this).show()
|
||||
// Reject change to setting by returning false
|
||||
return@OnPreferenceChangeListener false
|
||||
} else {
|
||||
if (preference == chkShowNotification && !newValue &&
|
||||
(PreferenceUtil.runInBackground(
|
||||
this,
|
||||
prefs
|
||||
) || PreferenceUtil.isFileLoggingEnabled(this, prefs))
|
||||
) {
|
||||
// Don't let the user disable notifications if background execution or logging is enabled
|
||||
createCanNotDisableSettingDialog(this).show()
|
||||
// Reject change to setting by returning false
|
||||
return@OnPreferenceChangeListener false
|
||||
}
|
||||
|
||||
if (preference == chkShowNotification && !newValue) {
|
||||
// If the user disabled the notification setting prompt them to restart app
|
||||
createRestartApplicationDialog(this).show()
|
||||
return@OnPreferenceChangeListener false
|
||||
}
|
||||
|
||||
// Accept change to setting by returning true
|
||||
return@OnPreferenceChangeListener true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(VERSION_CODES.TIRAMISU)
|
||||
fun createNotificationPermissionDialog(activity: Activity): Dialog {
|
||||
val view = activity.layoutInflater.inflate(R.layout.notification_permissions_dialog, null)
|
||||
val textView = view.findViewById<TextView>(R.id.notification_permission_instructions)
|
||||
textView.text = getString(R.string.notification_permission_required_dialog_text)
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.notification_permission_required_dialog_title)
|
||||
.setCancelable(false)
|
||||
.setView(view)
|
||||
.setPositiveButton(
|
||||
R.string.ok
|
||||
) { _: DialogInterface?, _: Int -> requestNotificationPermission() }
|
||||
.setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int -> }
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
@RequiresApi(VERSION_CODES.TIRAMISU)
|
||||
fun createCanNotDisableSettingDialog(activity: Activity): Dialog {
|
||||
val view = activity.layoutInflater.inflate(R.layout.notification_permissions_dialog, null)
|
||||
val textView = view.findViewById<TextView>(R.id.notification_permission_instructions)
|
||||
textView.text = getString(R.string.can_not_disable_setting_dialog_text)
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.notification_permission_required_dialog_title)
|
||||
.setCancelable(false)
|
||||
.setView(view)
|
||||
.setPositiveButton(
|
||||
R.string.ok
|
||||
) { _: DialogInterface?, _: Int -> requestNotificationPermission() }
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
@RequiresApi(VERSION_CODES.TIRAMISU)
|
||||
fun createRestartApplicationDialog(activity: Activity): Dialog {
|
||||
val view = activity.layoutInflater.inflate(R.layout.notification_permissions_dialog, null)
|
||||
val textView = view.findViewById<TextView>(R.id.notification_permission_instructions)
|
||||
textView.text = getString(R.string.need_to_restart_application_dialog_text)
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.need_to_restart_application_dialog_title)
|
||||
.setCancelable(false)
|
||||
.setView(view)
|
||||
.setPositiveButton(
|
||||
R.string.ok
|
||||
) { _: DialogInterface?, _: Int -> revokeNotificationPermissionAndRestartApplication() }
|
||||
.setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int -> }
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
@RequiresApi(api = VERSION_CODES.TIRAMISU)
|
||||
private fun requestNotificationPermission() {
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(PermissionUtils.getNotificationPermission()),
|
||||
PermissionUtils.NOTIFICATION_PERMISSION_REQUEST
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int, permissions: Array<String>, grantResults: IntArray,
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode == PermissionUtils.NOTIFICATION_PERMISSION_REQUEST) {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
// Notification permission granted - change the setting in the Preferences UI
|
||||
// The notification will automatically be posted by the service
|
||||
PreferenceUtils.saveBoolean(
|
||||
getString(R.string.pref_key_show_notification),
|
||||
true,
|
||||
prefs
|
||||
)
|
||||
recreate()
|
||||
} else {
|
||||
// Prompt the user to grant permissions again
|
||||
createNotificationPermissionDialog(this).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(VERSION_CODES.TIRAMISU)
|
||||
private fun Context.revokeNotificationPermissionAndRestartApplication() {
|
||||
revokeSelfPermissionOnKill(Manifest.permission.POST_NOTIFICATIONS)
|
||||
PreferenceUtils.saveBoolean(getString(R.string.pref_key_show_notification), false, prefs)
|
||||
|
||||
Executors.newSingleThreadScheduledExecutor().schedule({
|
||||
val intent = packageManager.getLaunchIntentForPackage(packageName)
|
||||
val componentName = intent?.component
|
||||
val mainIntent = Intent.makeRestartActivityTask(componentName)
|
||||
startActivity(mainIntent)
|
||||
Runtime.getRuntime().exit(0)
|
||||
}, 200, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.android.gpstest.ui.components
|
||||
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
* Auto-links any URLs within the provided [text]
|
||||
*
|
||||
* Code in this file is from https://stackoverflow.com/a/66235329/937715 by
|
||||
* https://stackoverflow.com/users/1737321/agonist under
|
||||
* https://creativecommons.org/licenses/by-sa/4.0/
|
||||
*
|
||||
* No changes have been made from the original code from StackOverflow.
|
||||
*/
|
||||
@Composable
|
||||
fun LinkifyText(text: String, modifier: Modifier = Modifier) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val layoutResult = remember {
|
||||
mutableStateOf<TextLayoutResult?>(null)
|
||||
}
|
||||
val linksList = extractUrls(text)
|
||||
val annotatedString = buildAnnotatedString {
|
||||
append(text)
|
||||
linksList.forEach {
|
||||
addStyle(
|
||||
style = SpanStyle(
|
||||
color = Color.Companion.Blue,
|
||||
textDecoration = TextDecoration.Underline
|
||||
),
|
||||
start = it.start,
|
||||
end = it.end
|
||||
)
|
||||
addStringAnnotation(
|
||||
tag = "URL",
|
||||
annotation = it.url,
|
||||
start = it.start,
|
||||
end = it.end
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(text = annotatedString, style = MaterialTheme.typography.body1, modifier = modifier.pointerInput(Unit) {
|
||||
detectTapGestures { offsetPosition ->
|
||||
layoutResult.value?.let {
|
||||
val position = it.getOffsetForPosition(offsetPosition)
|
||||
annotatedString.getStringAnnotations(position, position).firstOrNull()
|
||||
?.let { result ->
|
||||
if (result.tag == "URL") {
|
||||
uriHandler.openUri(result.item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onTextLayout = { layoutResult.value = it }
|
||||
)
|
||||
}
|
||||
|
||||
private val urlPattern: Pattern = Pattern.compile(
|
||||
"(?:^|[\\W])((ht|f)tp(s?):\\/\\/|www\\.)"
|
||||
+ "(([\\w\\-]+\\.){1,}?([\\w\\-.~]+\\/?)*"
|
||||
+ "[\\p{Alnum}.,%_=?&#\\-+()\\[\\]\\*$~@!:/{};']*)",
|
||||
Pattern.CASE_INSENSITIVE or Pattern.MULTILINE or Pattern.DOTALL
|
||||
)
|
||||
|
||||
fun extractUrls(text: String): List<LinkInfos> {
|
||||
val matcher = urlPattern.matcher(text)
|
||||
var matchStart: Int
|
||||
var matchEnd: Int
|
||||
val links = arrayListOf<LinkInfos>()
|
||||
|
||||
while (matcher.find()) {
|
||||
matchStart = matcher.start(1)
|
||||
matchEnd = matcher.end()
|
||||
|
||||
var url = text.substring(matchStart, matchEnd)
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://"))
|
||||
url = "https://$url"
|
||||
|
||||
links.add(LinkInfos(url, matchStart, matchEnd))
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
data class LinkInfos(
|
||||
val url: String,
|
||||
val start: Int,
|
||||
val end: Int
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.android.gpstest.dialog
|
||||
package com.android.gpstest.ui.share
|
||||
|
||||
import android.app.Dialog
|
||||
import android.net.Uri
|
||||
@@ -9,7 +9,7 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.android.gpstest.R
|
||||
import com.android.gpstest.dialog.ShareLogFragment.Listener
|
||||
import com.android.gpstest.ui.share.ShareLogFragment.Listener
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
|
||||
@@ -40,15 +40,17 @@ class ShareDialogFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val view = activity!!.layoutInflater.inflate(R.layout.share, null)
|
||||
val view = requireActivity().layoutInflater.inflate(R.layout.share, null)
|
||||
setRetainInstance(true)
|
||||
val builder = AlertDialog.Builder(activity!!)
|
||||
val builder = AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.share)
|
||||
.setView(view)
|
||||
.setNeutralButton(R.string.main_help_close) { dialog, _ -> }
|
||||
shareCollectionAdapter = ShareCollectionAdapter(this)
|
||||
shareCollectionAdapter.setArguments(arguments)
|
||||
shareCollectionAdapter.setListener(listener)
|
||||
if (this::listener.isInitialized) {
|
||||
shareCollectionAdapter.setListener(listener)
|
||||
}
|
||||
viewPager = view.findViewById(R.id.pager)
|
||||
viewPager.offscreenPageLimit = 2
|
||||
viewPager.adapter = shareCollectionAdapter
|
||||
@@ -80,6 +82,9 @@ class ShareDialogFragment : DialogFragment() {
|
||||
|
||||
fun setListener(listener: Listener) {
|
||||
this.listener = listener
|
||||
if (this::shareCollectionAdapter.isInitialized) {
|
||||
shareCollectionAdapter.setListener(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.android.gpstest.dialog
|
||||
package com.android.gpstest.ui.share
|
||||
|
||||
import android.content.Intent
|
||||
import android.location.Location
|
||||
@@ -13,10 +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.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.LibUIUtils
|
||||
import com.android.gpstest.library.util.PreferenceUtils
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
@@ -57,18 +60,23 @@ class ShareLocationFragment : Fragment() {
|
||||
}
|
||||
|
||||
// Set default state of include altitude view
|
||||
|
||||
// Set default state of include altitude view
|
||||
val includeAltitudePref = Application.getPrefs().getBoolean(Application.get().getString(R.string.pref_key_share_include_altitude), false)
|
||||
val includeAltitudePref = Application.prefs.getBoolean(Application.app.getString(R.string.pref_key_share_include_altitude), false)
|
||||
includeAltitude.isChecked = includeAltitudePref
|
||||
|
||||
// Check selected coordinate format and show in UI
|
||||
|
||||
// Check selected coordinate format and show in UI
|
||||
val coordinateFormat = Application.getPrefs().getString(Application.get().getString(R.string.pref_key_coordinate_format), Application.get().getString(R.string.preferences_coordinate_format_dd_key))
|
||||
UIUtils.formatLocationForDisplay(location, locationValue, includeAltitude.isChecked, chipDecimalDegrees, chipDMS, chipDegreesDecimalMin, coordinateFormat)
|
||||
|
||||
// Change the location text when the user toggles the altitude checkbox
|
||||
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) {
|
||||
LibUIUtils.formatLocationForDisplay(
|
||||
app,
|
||||
location,
|
||||
locationValue,
|
||||
includeAltitude.isChecked,
|
||||
chipDecimalDegrees,
|
||||
chipDMS,
|
||||
chipDegreesDecimalMin,
|
||||
coordinateFormat
|
||||
)
|
||||
}
|
||||
|
||||
// Change the location text when the user toggles the altitude checkbox
|
||||
includeAltitude.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
||||
@@ -80,30 +88,46 @@ class ShareLocationFragment : Fragment() {
|
||||
} else if (chipDegreesDecimalMin.isChecked) {
|
||||
format = "ddm"
|
||||
}
|
||||
UIUtils.formatLocationForDisplay(location, locationValue, isChecked, chipDecimalDegrees, chipDMS, chipDegreesDecimalMin, format)
|
||||
PreferenceUtils.saveBoolean(Application.get().getString(R.string.pref_key_share_include_altitude), isChecked)
|
||||
if (location != null) {
|
||||
LibUIUtils.formatLocationForDisplay(
|
||||
app,
|
||||
location,
|
||||
locationValue,
|
||||
isChecked,
|
||||
chipDecimalDegrees,
|
||||
chipDMS,
|
||||
chipDegreesDecimalMin,
|
||||
format
|
||||
)
|
||||
}
|
||||
PreferenceUtils.saveBoolean(Application.app.getString(R.string.pref_key_share_include_altitude), isChecked, prefs)
|
||||
}
|
||||
|
||||
chipDecimalDegrees.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
||||
if (isChecked) {
|
||||
locationValue.text = IOUtils.createLocationShare(location, includeAltitude.isChecked)
|
||||
if (location != null) {
|
||||
locationValue.text =
|
||||
IOUtils.createLocationShare(location, includeAltitude.isChecked)
|
||||
}
|
||||
}
|
||||
}
|
||||
chipDMS.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
||||
if (isChecked) {
|
||||
if (location != null) {
|
||||
locationValue.text = IOUtils.createLocationShare(UIUtils.getDMSFromLocation(Application.get(), location.getLatitude(), UIUtils.COORDINATE_LATITUDE),
|
||||
UIUtils.getDMSFromLocation(Application.get(), location.getLongitude(), UIUtils.COORDINATE_LONGITUDE),
|
||||
if (location.hasAltitude() && includeAltitude.isChecked) java.lang.Double.toString(location.getAltitude()) else null)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
chipDegreesDecimalMin.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
||||
if (isChecked) {
|
||||
if (location != null) {
|
||||
locationValue.text = IOUtils.createLocationShare(UIUtils.getDDMFromLocation(Application.get(), location.getLatitude(), UIUtils.COORDINATE_LATITUDE),
|
||||
UIUtils.getDDMFromLocation(Application.get(), location.getLongitude(), UIUtils.COORDINATE_LONGITUDE),
|
||||
if (location.hasAltitude() && includeAltitude.isChecked) java.lang.Double.toString(location.getAltitude()) else null)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,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()
|
||||
}
|
||||
}
|
||||
@@ -120,20 +144,20 @@ class ShareLocationFragment : Fragment() {
|
||||
// Open the browser to the GeoHack site with lots of coordinate conversions
|
||||
if (location != null) {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
val geohackUrl = Application.get().getString(R.string.geohack_url) +
|
||||
location.getLatitude() + ";" +
|
||||
location.getLongitude()
|
||||
val geohackUrl = Application.app.getString(R.string.geohack_url) +
|
||||
location.latitude + ";" +
|
||||
location.longitude
|
||||
intent.data = Uri.parse(geohackUrl)
|
||||
activity!!.startActivity(intent)
|
||||
requireActivity().startActivity(intent)
|
||||
}
|
||||
}
|
||||
locationLaunchApp.setOnClickListener { _: View? ->
|
||||
// Open the location in another app
|
||||
if (location != null) {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse(IOUtils.createGeoUri(location, includeAltitude.isChecked))
|
||||
if (intent.resolveActivity(activity!!.packageManager) != null) {
|
||||
activity!!.startActivity(intent)
|
||||
intent.data = Uri.parse(IOUtils.createGeoUri(app, location, includeAltitude.isChecked))
|
||||
if (intent.resolveActivity(requireActivity().packageManager) != null) {
|
||||
requireActivity().startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,15 +166,14 @@ class ShareLocationFragment : Fragment() {
|
||||
// selected, otherwise send plain text version
|
||||
if (location != null) {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
val text: String
|
||||
text = if (chipDecimalDegrees.isChecked) {
|
||||
IOUtils.createGeoUri(location, includeAltitude.isChecked)
|
||||
val text: String = if (chipDecimalDegrees.isChecked) {
|
||||
IOUtils.createGeoUri(app, location, includeAltitude.isChecked)
|
||||
} else {
|
||||
locationValue.text.toString()
|
||||
}
|
||||
intent.putExtra(Intent.EXTRA_TEXT, text)
|
||||
intent.type = "text/plain"
|
||||
activity!!.startActivity(Intent.createChooser(intent, Application.get().getString(R.string.share)))
|
||||
requireActivity().startActivity(Intent.createChooser(intent, Application.app.getString(R.string.share)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.android.gpstest.dialog
|
||||
package com.android.gpstest.ui.share
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
@@ -8,13 +8,14 @@ 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.BuildConfig
|
||||
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.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class ShareLogFragment : Fragment() {
|
||||
|
||||
@@ -85,10 +86,10 @@ class ShareLogFragment : Fragment() {
|
||||
|
||||
logBrowse.setOnClickListener { _: View? ->
|
||||
// File browse
|
||||
val uri = IOUtils.getUriFromFile(activity, files?.get(0))
|
||||
val uri = IOUtils.getUriFromFile(activity, BuildConfig.APPLICATION_ID, files?.get(0))
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
intent.data = uri
|
||||
activity!!.startActivityForResult(intent, UIUtils.PICKFILE_REQUEST_CODE)
|
||||
requireActivity().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, BuildConfig.APPLICATION_ID, 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()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.android.gpstest.dialog
|
||||
package com.android.gpstest.ui.share
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
@@ -18,17 +18,20 @@ import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
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.DeviceInfoViewModel
|
||||
import com.android.gpstest.R
|
||||
import com.android.gpstest.io.DevicePropertiesUploader
|
||||
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
|
||||
import java.io.IOException
|
||||
|
||||
@@ -42,45 +45,30 @@ class UploadDeviceInfoFragment : Fragment() {
|
||||
return inflater.inflate(R.layout.share_upload, container, false)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val uploadNoLocationTextView: TextView = view.findViewById(R.id.upload_no_location)
|
||||
val uploadGpsStatusApi: TextView = view.findViewById(R.id.upload_gps_status_api)
|
||||
val uploadDetails: TextView = view.findViewById(R.id.upload_details)
|
||||
val uploadProgress: ProgressBar = view.findViewById(R.id.upload_progress)
|
||||
val upload: MaterialButton = view.findViewById(R.id.upload)
|
||||
|
||||
val location = arguments?.getParcelable<Location>(ShareDialogFragment.KEY_LOCATION)
|
||||
val deviceInfoViewModel = ViewModelProviders.of(activity!!).get(DeviceInfoViewModel::class.java)
|
||||
val signalInfoViewModel: SignalInfoViewModel by activityViewModels()
|
||||
var userCountry = ""
|
||||
|
||||
val useGnssApis = Application.getPrefs().getBoolean(getString(R.string.pref_key_use_gnss_apis), true)
|
||||
|
||||
// TODO - DeviceInfoViewModel is still largely updated in GnssStatusFragment, so we need
|
||||
// to check and make sure that the Status screen has been viewed even if we have a location
|
||||
// to ensure we capture dual-frequency capability, supported GNSS, etc.
|
||||
// Future work should move DeviceInfoViewModel so it's contained in the Activity instead
|
||||
// so no matter what fragment is visible all the DeviceInfoViewModel are still updated.
|
||||
if (location == null || !deviceInfoViewModel.gotFirstFix()) {
|
||||
if (location == null || !signalInfoViewModel.gotFirstFix()) {
|
||||
// No location
|
||||
uploadDetails.visibility = View.GONE
|
||||
upload.visibility = View.GONE
|
||||
uploadNoLocationTextView.visibility = View.VISIBLE
|
||||
uploadGpsStatusApi.visibility = View.GONE
|
||||
} else if (SatelliteUtils.isGnssStatusListenerSupported() && !useGnssApis) {
|
||||
// Android 7 and higher but using legacy GPSStatus API
|
||||
uploadDetails.visibility = View.GONE
|
||||
upload.visibility = View.GONE
|
||||
uploadNoLocationTextView.visibility = View.GONE
|
||||
uploadGpsStatusApi.visibility = View.VISIBLE
|
||||
} else {
|
||||
// We have a location and using GNSS APIs if Android 7 or later
|
||||
// We have a location
|
||||
uploadDetails.visibility = View.VISIBLE
|
||||
upload.visibility = View.VISIBLE
|
||||
uploadNoLocationTextView.visibility = View.GONE
|
||||
uploadGpsStatusApi.visibility = View.GONE
|
||||
|
||||
if (Geocoder.isPresent()) {
|
||||
val geocoder = Geocoder(context)
|
||||
val geocoder = Geocoder(requireContext())
|
||||
var addresses: List<Address>? = emptyList()
|
||||
try {
|
||||
addresses = geocoder.getFromLocation(location.latitude, location.longitude, 1)
|
||||
@@ -99,70 +87,70 @@ class UploadDeviceInfoFragment : Fragment() {
|
||||
var versionName = ""
|
||||
var versionCode = ""
|
||||
try {
|
||||
val info: PackageInfo = Application.get().packageManager.getPackageInfo(Application.get().packageName, 0)
|
||||
val info: PackageInfo = Application.app.packageManager.getPackageInfo(Application.app.packageName, 0)
|
||||
versionName = info.versionName
|
||||
versionCode = info.versionCode.toString()
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
val locationManager = Application.get().getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
val locationManager = Application.app.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
|
||||
// Inject PSDS capability
|
||||
val capabilityInjectPsdsInt = Application.getPrefs().getInt(Application.get().getString(R.string.capability_key_inject_psds), PreferenceUtils.CAPABILITY_UNKNOWN)
|
||||
val capabilityInjectPsdsInt = Application.prefs.getInt(Application.app.getString(R.string.capability_key_inject_psds), PreferenceUtils.CAPABILITY_UNKNOWN)
|
||||
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
|
||||
val capabilityInjectTimeInt = Application.getPrefs().getInt(Application.get().getString(R.string.capability_key_inject_time), PreferenceUtils.CAPABILITY_UNKNOWN)
|
||||
val capabilityInjectTimeInt = Application.prefs.getInt(Application.app.getString(R.string.capability_key_inject_time), PreferenceUtils.CAPABILITY_UNKNOWN)
|
||||
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
|
||||
val capabilityDeleteAssistInt = Application.getPrefs().getInt(Application.get().getString(R.string.capability_key_delete_assist), PreferenceUtils.CAPABILITY_UNKNOWN)
|
||||
val capabilityDeleteAssistInt = Application.prefs.getInt(Application.app.getString(R.string.capability_key_delete_assist), PreferenceUtils.CAPABILITY_UNKNOWN)
|
||||
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 = ""
|
||||
}
|
||||
|
||||
// GNSS measurements
|
||||
val capabilityMeasurementsInt = Application.getPrefs().getInt(Application.get().getString(R.string.capability_key_raw_measurements), PreferenceUtils.CAPABILITY_UNKNOWN)
|
||||
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 = ""
|
||||
}
|
||||
|
||||
// GNSS navigation message
|
||||
val capabilityNavMessagesInt = Application.getPrefs().getInt(Application.get().getString(R.string.capability_key_nav_messages), PreferenceUtils.CAPABILITY_UNKNOWN)
|
||||
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.get().getString(R.string.capability_value_supported))) {
|
||||
numAntennas = PreferenceUtils.getInt(Application.get().getString(R.string.capability_key_num_antenna), -1).toString()
|
||||
antennaCfs = PreferenceUtils.getString(Application.get().getString(R.string.capability_key_antenna_cf))
|
||||
if (gnssAntennaInfo.equals(Application.app.getString(R.string.capability_value_supported))) {
|
||||
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 = ""
|
||||
@@ -175,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(deviceInfoViewModel.isNonPrimaryCarrierFreqInView),
|
||||
DevicePropertiesUploader.SUPPORTED_GNSS to trimEnds(replaceNavstar(deviceInfoViewModel.supportedGnss.sorted().toString())),
|
||||
DevicePropertiesUploader.GNSS_CFS to trimEnds(deviceInfoViewModel.supportedGnssCfs.sorted().toString()),
|
||||
DevicePropertiesUploader.SUPPORTED_SBAS to trimEnds(deviceInfoViewModel.supportedSbas.sorted().toString()),
|
||||
DevicePropertiesUploader.SBAS_CFS to trimEnds(deviceInfoViewModel.supportedSbasCfs.sorted().toString()),
|
||||
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.getPrefs().getInt(Application.get().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.getPrefs().getInt(Application.get().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.getPrefs().getInt(Application.get().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,10 +193,10 @@ class UploadDeviceInfoFragment : Fragment() {
|
||||
upload.isEnabled = false
|
||||
|
||||
// Check to see if anything changed since last upload
|
||||
val lastUpload = Application.getPrefs().getInt(Application.get().getString(R.string.capability_key_last_upload_hash), Int.MAX_VALUE)
|
||||
val lastUpload = Application.prefs.getInt(Application.app.getString(R.string.capability_key_last_upload_hash), Int.MAX_VALUE)
|
||||
if (lastUpload != Int.MAX_VALUE && lastUpload == bundle.toString().hashCode()) {
|
||||
// Nothing changed since last upload
|
||||
Toast.makeText(Application.get(), R.string.upload_nothing_changed, Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(Application.app, R.string.upload_nothing_changed, Toast.LENGTH_SHORT).show()
|
||||
upload.isEnabled = true
|
||||
} else {
|
||||
// First upload, or something changed since last upload - add app version and upload data
|
||||
@@ -219,13 +207,13 @@ class UploadDeviceInfoFragment : Fragment() {
|
||||
lifecycle.coroutineScope.launch {
|
||||
val uploader = DevicePropertiesUploader(bundle)
|
||||
if (uploader.upload()) {
|
||||
Toast.makeText(Application.get(), 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.get().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.get(), 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
|
||||
590
GPSTest/src/main/java/com/android/gpstest/ui/sky/SkyFragment.kt
Normal file
@@ -0,0 +1,590 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
package com.android.gpstest.ui.sky
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.Transformation
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import androidx.lifecycle.Lifecycle
|
||||
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.databinding.GpsSkyBinding
|
||||
import com.android.gpstest.databinding.GpsSkyLegendCardBinding
|
||||
import com.android.gpstest.databinding.GpsSkySignalMeterBinding
|
||||
import com.android.gpstest.library.data.FixState
|
||||
import com.android.gpstest.library.data.LocationRepository
|
||||
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.library.util.LibUIUtils
|
||||
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.ui.status.Filter
|
||||
import com.android.gpstest.ui.theme.AppTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SkyFragment : Fragment() {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val viewModel: SignalInfoViewModel by activityViewModels()
|
||||
|
||||
// Binding variables
|
||||
private var _binding: GpsSkyBinding? = null
|
||||
private val binding get() = _binding
|
||||
private lateinit var legendLines: List<View>
|
||||
private lateinit var legendShapes: List<ImageView>
|
||||
private lateinit var meter: GpsSkySignalMeterBinding
|
||||
private lateinit var legend: GpsSkyLegendCardBinding
|
||||
|
||||
// Animations for cn0 indicators
|
||||
private var cn0InViewAvgAnimation: Animation? = null
|
||||
var cn0UsedAvgAnimation: Animation? = null
|
||||
var cn0InViewAvgAnimationTextView: Animation? = null
|
||||
var cn0UsedAvgAnimationTextView: Animation? = null
|
||||
|
||||
// Default light theme values
|
||||
private var usedCn0Background = R.drawable.cn0_round_corner_background_used
|
||||
private var usedCn0IndicatorColor = Color.BLACK
|
||||
|
||||
// Repository of location data that the service will observe, injected via Hilt
|
||||
@Inject
|
||||
lateinit var repository: LocationRepository
|
||||
|
||||
// Get a reference to the Job from the Flow so we can stop it from UI events
|
||||
private var gnssFlow: Job? = null
|
||||
private var sensorFlow: Job? = null
|
||||
|
||||
// Preference listener that will cancel the above flows when the user turns off tracking via UI
|
||||
private val trackingListener: SharedPreferences.OnSharedPreferenceChangeListener =
|
||||
PreferenceUtil.newStopTrackingListener ({ onGnssStopped() }, prefs)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = GpsSkyBinding.inflate(inflater, container, false)
|
||||
val v = binding!!.root
|
||||
meter = binding!!.skyCn0IndicatorCard.gpsSkySignalMeter
|
||||
legend = binding!!.skyLegendCard
|
||||
|
||||
initFilterView(viewModel)
|
||||
|
||||
initLegendViews()
|
||||
|
||||
Application.prefs.registerOnSharedPreferenceChangeListener(trackingListener)
|
||||
|
||||
observeLocationUpdateStates()
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val color: Int
|
||||
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)
|
||||
usedCn0Background = R.drawable.cn0_round_corner_background_used_dark
|
||||
usedCn0IndicatorColor = resources.getColor(android.R.color.darker_gray)
|
||||
} else {
|
||||
// Light theme
|
||||
color = ContextCompat.getColor(requireContext(), R.color.body_text_2_light)
|
||||
legend.skyLegendUsedInFix.setImageResource(R.drawable.circle_used_in_fix)
|
||||
usedCn0Background = R.drawable.cn0_round_corner_background_used
|
||||
usedCn0IndicatorColor = Color.BLACK
|
||||
}
|
||||
for (v in legendLines) {
|
||||
v.setBackgroundColor(color)
|
||||
}
|
||||
for (v in legendShapes) {
|
||||
v.setColorFilter(color)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeLocationUpdateStates() {
|
||||
repository.receivingLocationUpdates
|
||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
||||
.onEach {
|
||||
when (it) {
|
||||
true -> onGnssStarted()
|
||||
false -> onGnssStopped()
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeGnssStatus() {
|
||||
val gnssStatusObserver = Observer<List<SatelliteStatus>> { statuses ->
|
||||
updateGnssStatus(statuses)
|
||||
}
|
||||
|
||||
viewModel.filteredStatuses.observe(
|
||||
viewLifecycleOwner, gnssStatusObserver
|
||||
)
|
||||
}
|
||||
|
||||
private fun observeGnssStates() {
|
||||
repository.fixState
|
||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
||||
.onEach {
|
||||
when (it) {
|
||||
is FixState.Acquired -> onGnssFixAcquired()
|
||||
is FixState.NotAcquired -> if (PreferenceUtils.isTrackingStarted(prefs)) onGnssFixLost()
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeSensorFlow() {
|
||||
if (sensorFlow?.isActive == true) {
|
||||
// If we're already observing updates, don't register again
|
||||
return
|
||||
}
|
||||
// Observe locations via Flow as they are generated by the repository
|
||||
sensorFlow = repository.getSensorUpdates()
|
||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
||||
.onEach {
|
||||
//Log.d(TAG, "Sky sensor: orientation ${it[0]}, tilt ${it[1]}")
|
||||
onOrientationChanged(it.values[0], it.values[1])
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private fun onGnssFixAcquired() {
|
||||
showHaveFix()
|
||||
}
|
||||
|
||||
private fun onGnssFixLost() {
|
||||
showLostFix()
|
||||
}
|
||||
|
||||
private fun updateGnssStatus(statuses: List<SatelliteStatus>) {
|
||||
binding?.skyView?.setStatus(statuses)
|
||||
updateCn0AvgMeterText()
|
||||
updateCn0Avgs()
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun onGnssStarted() {
|
||||
binding?.skyView?.setStarted()
|
||||
// Activity or service is observing updates, so observe here too
|
||||
observeGnssStatus()
|
||||
observeGnssStates()
|
||||
observeSensorFlow()
|
||||
}
|
||||
|
||||
private fun onGnssStopped() {
|
||||
// Cancel updates (Note that these are canceled via trackingListener preference listener
|
||||
// in the case where updates are stopped from the Activity UI switch).
|
||||
sensorFlow?.cancel()
|
||||
gnssFlow?.cancel()
|
||||
|
||||
binding?.skyView?.setStopped()
|
||||
binding?.skyLock?.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun onOrientationChanged(orientation: Double, tilt: Double) {
|
||||
// For performance reasons, only proceed if this fragment is visible
|
||||
// TODO - this is now deprecated and a no-op, update to newest code
|
||||
if (!userVisibleHint) {
|
||||
return
|
||||
}
|
||||
binding?.skyView?.onOrientationChanged(orientation, tilt)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun initFilterView(viewModel: SignalInfoViewModel) {
|
||||
binding!!.filterView.apply {
|
||||
// Dispose the Composition when the view's LifecycleOwner is destroyed
|
||||
setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
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(app, prefs).isNotEmpty()) {
|
||||
Filter(allStatuses.size, satelliteMetadata) { clearGnssFilter(app, prefs) }
|
||||
}
|
||||
}
|
||||
}
|
||||
id = R.id.filter_view
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the views in the C/N0 and Shape legends
|
||||
*/
|
||||
private fun initLegendViews() {
|
||||
// Avg C/N0 indicator lines
|
||||
val cn0 = meter.signalMeterTicksAndText
|
||||
legendLines = listOf(
|
||||
cn0.skyLegendCn0LeftLine4,
|
||||
cn0.skyLegendCn0LeftLine3,
|
||||
cn0.skyLegendCn0LeftLine2,
|
||||
cn0.skyLegendCn0LeftLine1,
|
||||
cn0.skyLegendCn0CenterLine,
|
||||
cn0.skyLegendCn0RightLine1,
|
||||
cn0.skyLegendCn0RightLine2,
|
||||
cn0.skyLegendCn0RightLine3,
|
||||
cn0.skyLegendCn0RightLine4,
|
||||
legend.skyLegendShapeLine1a,
|
||||
legend.skyLegendShapeLine1b,
|
||||
legend.skyLegendShapeLine2a,
|
||||
legend.skyLegendShapeLine2b,
|
||||
legend.skyLegendShapeLine3a,
|
||||
legend.skyLegendShapeLine3b,
|
||||
legend.skyLegendShapeLine4a,
|
||||
legend.skyLegendShapeLine4b,
|
||||
legend.skyLegendShapeLine5a,
|
||||
legend.skyLegendShapeLine5b,
|
||||
legend.skyLegendShapeLine6a,
|
||||
legend.skyLegendShapeLine6b,
|
||||
legend.skyLegendShapeLine7a,
|
||||
legend.skyLegendShapeLine7b,
|
||||
legend.skyLegendShapeLine8a,
|
||||
legend.skyLegendShapeLine8b,
|
||||
legend.skyLegendShapeLine9a,
|
||||
legend.skyLegendShapeLine9b,
|
||||
legend.skyLegendShapeLine10a,
|
||||
legend.skyLegendShapeLine10b,
|
||||
legend.skyLegendShapeLine11a,
|
||||
legend.skyLegendShapeLine12a,
|
||||
legend.skyLegendShapeLine13a,
|
||||
legend.skyLegendShapeLine14a,
|
||||
legend.skyLegendShapeLine14b,
|
||||
legend.skyLegendShapeLine15a,
|
||||
legend.skyLegendShapeLine15b,
|
||||
legend.skyLegendShapeLine16a,
|
||||
legend.skyLegendShapeLine16b,
|
||||
legend.skyLegendShapeLine17a,
|
||||
legend.skyLegendShapeLine17b
|
||||
)
|
||||
|
||||
// Shape Legend shapes
|
||||
legendShapes = listOf(
|
||||
legend.skyLegendCircle,
|
||||
legend.skyLegendSquare,
|
||||
legend.skyLegendPentagon,
|
||||
legend.skyLegendTriangle,
|
||||
legend.skyLegendHexagon1,
|
||||
legend.skyLegendOval,
|
||||
legend.skyLegendDiamond1,
|
||||
legend.skyLegendDiamond2,
|
||||
legend.skyLegendDiamond3,
|
||||
legend.skyLegendDiamond4,
|
||||
legend.skyLegendDiamond5,
|
||||
legend.skyLegendDiamond6,
|
||||
legend.skyLegendDiamond7,
|
||||
legend.skyLegendDiamond8
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateCn0AvgMeterText() {
|
||||
binding?.skyCn0IndicatorCard?.gpsSkySignalTitle?.apply {
|
||||
skyLegendCn0Title.setText(R.string.gps_cn0_column_label)
|
||||
skyLegendCn0Units.setText(R.string.sky_legend_cn0_units)
|
||||
}
|
||||
meter.signalMeterTicksAndText.apply {
|
||||
skyLegendCn0LeftText.setText(R.string.sky_legend_cn0_low)
|
||||
skyLegendCn0LeftCenterText.setText(R.string.sky_legend_cn0_low_middle)
|
||||
skyLegendCn0CenterText.setText(R.string.sky_legend_cn0_middle)
|
||||
skyLegendCn0RightCenterText.setText(R.string.sky_legend_cn0_middle_high)
|
||||
skyLegendCn0RightText.setText(R.string.sky_legend_cn0_high)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCn0Avgs() {
|
||||
if (binding == null) {
|
||||
return
|
||||
}
|
||||
// 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()
|
||||
- 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
|
||||
val minTextViewMarginPx = Application.app.resources
|
||||
.getDimension(R.dimen.cn0_textview_min_left_margin).toInt()
|
||||
val maxTextViewMarginPx = meterWidthPx + minTextViewMarginPx
|
||||
|
||||
// When both "in view" and "used" indicators and TextViews are shown, slide the "in view" TextView by this amount to the left to avoid overlap
|
||||
val TEXTVIEW_NON_OVERLAP_OFFSET_DP = -16.0f
|
||||
|
||||
// Calculate normal offsets for avg in view satellite C/N0 value TextViews
|
||||
var leftInViewTextViewMarginPx: Int? = null
|
||||
if (MathUtils.isValidFloat(binding!!.skyView.cn0InViewAvg)) {
|
||||
leftInViewTextViewMarginPx = LibUIUtils.cn0ToTextViewLeftMarginPx(
|
||||
binding!!.skyView.cn0InViewAvg,
|
||||
minTextViewMarginPx, maxTextViewMarginPx
|
||||
)
|
||||
}
|
||||
|
||||
// Calculate normal offsets for avg used satellite C/N0 value TextViews
|
||||
var leftUsedTextViewMarginPx: Int? = null
|
||||
if (MathUtils.isValidFloat(binding!!.skyView.cn0UsedAvg)) {
|
||||
leftUsedTextViewMarginPx = LibUIUtils.cn0ToTextViewLeftMarginPx(
|
||||
binding!!.skyView.cn0UsedAvg,
|
||||
minTextViewMarginPx, maxTextViewMarginPx
|
||||
)
|
||||
}
|
||||
|
||||
// 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 = LibUIUtils.dpToPixels(Application.app, TEXTVIEW_NON_OVERLAP_OFFSET_DP)
|
||||
if (leftInViewTextViewMarginPx <= leftUsedTextViewMarginPx) {
|
||||
leftInViewTextViewMarginPx += offset
|
||||
} else {
|
||||
leftUsedTextViewMarginPx += offset
|
||||
}
|
||||
}
|
||||
|
||||
// Define paddings used for TextViews
|
||||
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)) {
|
||||
meter.cn0TextInView.cn0TextInView.text =
|
||||
String.format("%.1f", binding!!.skyView.cn0InViewAvg)
|
||||
|
||||
// Set color of TextView
|
||||
val color = binding!!.skyView.getSatelliteColor(binding!!.skyView.cn0InViewAvg)
|
||||
val background = ContextCompat.getDrawable(
|
||||
Application.app,
|
||||
R.drawable.cn0_round_corner_background_in_view
|
||||
) as LayerDrawable?
|
||||
|
||||
// Fill
|
||||
val backgroundGradient =
|
||||
background!!.findDrawableByLayerId(R.id.cn0_avg_in_view_fill) as GradientDrawable
|
||||
backgroundGradient.setColor(color)
|
||||
|
||||
// Stroke
|
||||
val borderGradient =
|
||||
background.findDrawableByLayerId(R.id.cn0_avg_in_view_border) as GradientDrawable
|
||||
borderGradient.setColor(color)
|
||||
meter.cn0TextInView.cn0TextInView.background = background
|
||||
|
||||
// Set padding
|
||||
meter.cn0TextInView.cn0TextInView.setPadding(pSides, pTopBottom, pSides, pTopBottom)
|
||||
|
||||
// Set color of indicator
|
||||
meter.cn0IndicatorInView.setColorFilter(color)
|
||||
|
||||
// Set position and visibility of TextView
|
||||
if (meter.cn0TextInView.cn0TextInView.visibility == View.VISIBLE) {
|
||||
animateCn0Indicator(
|
||||
meter.cn0TextInView.cn0TextInView,
|
||||
leftInViewTextViewMarginPx!!,
|
||||
cn0InViewAvgAnimationTextView
|
||||
)
|
||||
} else {
|
||||
val lp =
|
||||
meter.cn0TextInView.cn0TextInView.layoutParams as RelativeLayout.LayoutParams
|
||||
lp.setMargins(
|
||||
leftInViewTextViewMarginPx!!,
|
||||
lp.topMargin,
|
||||
lp.rightMargin,
|
||||
lp.bottomMargin
|
||||
)
|
||||
meter.cn0TextInView.cn0TextInView.layoutParams = lp
|
||||
meter.cn0TextInView.cn0TextInView.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
// Set position and visibility of indicator
|
||||
val leftIndicatorMarginPx = LibUIUtils.cn0ToIndicatorLeftMarginPx(
|
||||
binding!!.skyView.cn0InViewAvg,
|
||||
minIndicatorMarginPx, maxIndicatorMarginPx
|
||||
)
|
||||
|
||||
// If the view is already visible, animate to the new position. Otherwise just set the position and make it visible
|
||||
if (meter.cn0IndicatorInView.visibility == View.VISIBLE) {
|
||||
animateCn0Indicator(
|
||||
meter.cn0IndicatorInView,
|
||||
leftIndicatorMarginPx,
|
||||
cn0InViewAvgAnimation
|
||||
)
|
||||
} else {
|
||||
val lp = meter.cn0IndicatorInView.layoutParams as RelativeLayout.LayoutParams
|
||||
lp.setMargins(leftIndicatorMarginPx, lp.topMargin, lp.rightMargin, lp.bottomMargin)
|
||||
meter.cn0IndicatorInView.layoutParams = lp
|
||||
meter.cn0IndicatorInView.visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
meter.cn0TextInView.cn0TextInView.text = ""
|
||||
meter.cn0TextInView.cn0TextInView.visibility = View.INVISIBLE
|
||||
meter.cn0IndicatorInView.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
// Set avg C/N0 of satellites used in fix
|
||||
if (MathUtils.isValidFloat(binding!!.skyView.cn0UsedAvg)) {
|
||||
meter.cn0TextUsed.cn0TextUsed.text = String.format("%.1f", binding!!.skyView.cn0UsedAvg)
|
||||
// Set color of TextView
|
||||
val color = binding!!.skyView.getSatelliteColor(binding!!.skyView.cn0UsedAvg)
|
||||
val background =
|
||||
ContextCompat.getDrawable(Application.app, usedCn0Background) as LayerDrawable?
|
||||
|
||||
// Fill
|
||||
val backgroundGradient =
|
||||
background!!.findDrawableByLayerId(R.id.cn0_avg_used_fill) as GradientDrawable
|
||||
backgroundGradient.setColor(color)
|
||||
meter.cn0TextUsed.cn0TextUsed.background = background
|
||||
|
||||
// Set padding
|
||||
meter.cn0TextUsed.cn0TextUsed.setPadding(pSides, pTopBottom, pSides, pTopBottom)
|
||||
|
||||
// Set color of indicator
|
||||
meter.cn0IndicatorUsed.setColorFilter(usedCn0IndicatorColor)
|
||||
|
||||
// Set position and visibility of TextView
|
||||
if (meter.cn0TextUsed.cn0TextUsed.visibility == View.VISIBLE) {
|
||||
animateCn0Indicator(
|
||||
meter.cn0TextUsed.cn0TextUsed,
|
||||
leftUsedTextViewMarginPx!!,
|
||||
cn0UsedAvgAnimationTextView
|
||||
)
|
||||
} else {
|
||||
val lp = meter.cn0TextUsed.cn0TextUsed.layoutParams as RelativeLayout.LayoutParams
|
||||
lp.setMargins(
|
||||
leftUsedTextViewMarginPx!!,
|
||||
lp.topMargin,
|
||||
lp.rightMargin,
|
||||
lp.bottomMargin
|
||||
)
|
||||
meter.cn0TextUsed.cn0TextUsed.layoutParams = lp
|
||||
meter.cn0TextUsed.cn0TextUsed.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
// Set position and visibility of indicator
|
||||
val leftMarginPx = LibUIUtils.cn0ToIndicatorLeftMarginPx(
|
||||
binding!!.skyView.cn0UsedAvg,
|
||||
minIndicatorMarginPx, maxIndicatorMarginPx
|
||||
)
|
||||
|
||||
// If the view is already visible, animate to the new position. Otherwise just set the position and make it visible
|
||||
if (meter.cn0IndicatorUsed.visibility == View.VISIBLE) {
|
||||
animateCn0Indicator(meter.cn0IndicatorUsed, leftMarginPx, cn0UsedAvgAnimation)
|
||||
} else {
|
||||
val lp = meter.cn0IndicatorUsed.layoutParams as RelativeLayout.LayoutParams
|
||||
lp.setMargins(leftMarginPx, lp.topMargin, lp.rightMargin, lp.bottomMargin)
|
||||
meter.cn0IndicatorUsed.layoutParams = lp
|
||||
meter.cn0IndicatorUsed.visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
meter.cn0TextUsed.cn0TextUsed.text = ""
|
||||
meter.cn0TextUsed.cn0TextUsed.visibility = View.INVISIBLE
|
||||
meter.cn0IndicatorUsed.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animates a C/N0 indicator view from it's current location to the provided left margin location (in pixels)
|
||||
* @param v view to animate
|
||||
* @param goalLeftMarginPx the new left margin for the view that the view should animate to in pixels
|
||||
* @param animation Animation to use for the animation
|
||||
*/
|
||||
private fun animateCn0Indicator(v: View?, goalLeftMarginPx: Int, animation: Animation?) {
|
||||
if (v == null) {
|
||||
return
|
||||
}
|
||||
var mutableAnimation = animation
|
||||
mutableAnimation?.reset()
|
||||
val p = v.layoutParams as MarginLayoutParams
|
||||
val currentMargin = p.leftMargin
|
||||
mutableAnimation = object : Animation() {
|
||||
override fun applyTransformation(interpolatedTime: Float, t: Transformation) {
|
||||
val newLeft: Int = if (goalLeftMarginPx > currentMargin) {
|
||||
currentMargin + (abs(currentMargin - goalLeftMarginPx)
|
||||
* interpolatedTime).toInt()
|
||||
} else {
|
||||
currentMargin - (abs(currentMargin - goalLeftMarginPx)
|
||||
* interpolatedTime).toInt()
|
||||
}
|
||||
LibUIUtils.setMargins(
|
||||
v,
|
||||
newLeft,
|
||||
p.topMargin,
|
||||
p.rightMargin,
|
||||
p.bottomMargin
|
||||
)
|
||||
}
|
||||
}
|
||||
// C/N0 updates every second, so animation of 300ms (https://material.io/guidelines/motion/duration-easing.html#duration-easing-common-durations)
|
||||
// wit FastOutSlowInInterpolator recommended by Material Design spec easily finishes in time for next C/N0 update
|
||||
mutableAnimation.setDuration(300)
|
||||
mutableAnimation.setInterpolator(FastOutSlowInInterpolator())
|
||||
v.startAnimation(mutableAnimation)
|
||||
}
|
||||
|
||||
private fun showHaveFix() {
|
||||
binding?.let { LibUIUtils.showViewWithAnimation(it.skyLock, LibUIUtils.ANIMATION_DURATION_SHORT_MS) }
|
||||
}
|
||||
|
||||
private fun showLostFix() {
|
||||
binding?.let { LibUIUtils.hideViewWithAnimation(it.skyLock, LibUIUtils.ANIMATION_DURATION_SHORT_MS) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "GpsSkyFragment"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.ui.status
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import android.os.Build
|
||||
import android.text.TextUtils
|
||||
import android.text.format.DateFormat
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
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.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.DateTimeUtils
|
||||
import com.android.gpstest.library.util.FormatUtils.formatAccuracy
|
||||
import com.android.gpstest.library.util.FormatUtils.formatAltitude
|
||||
import com.android.gpstest.library.util.FormatUtils.formatAltitudeMsl
|
||||
import com.android.gpstest.library.util.FormatUtils.formatBearing
|
||||
import com.android.gpstest.library.util.FormatUtils.formatBearingAccuracy
|
||||
import com.android.gpstest.library.util.FormatUtils.formatDoP
|
||||
import com.android.gpstest.library.util.FormatUtils.formatHvDOP
|
||||
import com.android.gpstest.library.util.FormatUtils.formatLatOrLon
|
||||
import com.android.gpstest.library.util.FormatUtils.formatNumSats
|
||||
import com.android.gpstest.library.util.FormatUtils.formatSpeed
|
||||
import com.android.gpstest.library.util.FormatUtils.formatSpeedAccuracy
|
||||
import com.android.gpstest.library.util.IOUtils
|
||||
import com.android.gpstest.library.util.LibUIUtils
|
||||
import com.android.gpstest.library.util.PreferenceUtil.coordinateFormat
|
||||
import com.android.gpstest.library.util.PreferenceUtil.shareIncludeAltitude
|
||||
import com.android.gpstest.library.util.PreferenceUtils
|
||||
import com.android.gpstest.library.util.PreferenceUtils.gnssFilter
|
||||
import com.android.gpstest.library.util.SatelliteUtil.isVerticalAccuracySupported
|
||||
import com.android.gpstest.ui.components.LinkifyText
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LocationCardPreview(
|
||||
@PreviewParameter(LocationPreviewParameterProvider::class) location: Location
|
||||
) {
|
||||
LocationCard(
|
||||
location,
|
||||
"5 sec",
|
||||
1.4,
|
||||
DilutionOfPrecision(1.0, 2.0, 3.0),
|
||||
SatelliteMetadata(),
|
||||
FixState.Acquired
|
||||
)
|
||||
}
|
||||
|
||||
class LocationPreviewParameterProvider : PreviewParameterProvider<Location> {
|
||||
override val values = sequenceOf(previewLocation())
|
||||
}
|
||||
|
||||
fun previewLocation(): Location {
|
||||
val l = Location("preview")
|
||||
l.apply {
|
||||
latitude = 28.38473847
|
||||
longitude = -87.32837456
|
||||
time = 1633375741711
|
||||
altitude = 13.5
|
||||
speed = 21.5f
|
||||
bearing = 240f
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
bearingAccuracyDegrees = 5.6f
|
||||
speedAccuracyMetersPerSecond = 6.1f
|
||||
verticalAccuracyMeters = 92.5f
|
||||
}
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LocationCard(
|
||||
location: Location,
|
||||
ttff: String,
|
||||
altitudeMsl: Double,
|
||||
dop: DilutionOfPrecision,
|
||||
satelliteMetadata: SatelliteMetadata,
|
||||
fixState: FixState,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(5.dp)
|
||||
.clickable {
|
||||
copyToClipboard(context, location)
|
||||
},
|
||||
elevation = 2.dp
|
||||
) {
|
||||
Box {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.horizontalScroll(rememberScrollState())
|
||||
) {
|
||||
LabelColumn1()
|
||||
ValueColumn1(context, location, altitudeMsl, dop)
|
||||
LabelColumn2(location)
|
||||
ValueColumn2(location, ttff, dop, satelliteMetadata)
|
||||
}
|
||||
LockIcon(fixState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ValueColumn1(
|
||||
context: Context,
|
||||
location: Location,
|
||||
altitudeMsl: Double,
|
||||
dop: DilutionOfPrecision,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.wrapContentHeight()
|
||||
.wrapContentWidth()
|
||||
.padding(top = 5.dp, bottom = 5.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Latitude(location)
|
||||
Longitude(location)
|
||||
Altitude(location)
|
||||
AltitudeMsl(altitudeMsl)
|
||||
Speed(location)
|
||||
SpeedAccuracy(location)
|
||||
Pdop(dop)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Latitude(location: Location) {
|
||||
LocationValue(formatLatOrLon(app, location.latitude, CoordinateType.LATITUDE, prefs))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Longitude(location: Location) {
|
||||
LocationValue(formatLatOrLon(app, location.longitude, CoordinateType.LONGITUDE, prefs))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Altitude(location: Location) {
|
||||
LocationValue(formatAltitude(app, location, prefs))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AltitudeMsl(altitudeMsl: Double) {
|
||||
LocationValue(formatAltitudeMsl(app, altitudeMsl, prefs))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Speed(location: Location) {
|
||||
LocationValue(formatSpeed(app, location, prefs))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SpeedAccuracy(location: Location) {
|
||||
LocationValue(formatSpeedAccuracy(app, location, prefs))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Pdop(dop: DilutionOfPrecision) {
|
||||
LocationValue(formatDoP(app, dop = dop))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ValueColumn2(
|
||||
location: Location,
|
||||
ttff: String,
|
||||
dop: DilutionOfPrecision,
|
||||
satelliteMetadata: SatelliteMetadata,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.wrapContentHeight()
|
||||
.wrapContentWidth()
|
||||
.padding(top = 5.dp, bottom = 5.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Time(location)
|
||||
TTFF(ttff)
|
||||
Accuracy(location)
|
||||
NumSats(satelliteMetadata)
|
||||
Bearing(location)
|
||||
BearingAccuracy(location)
|
||||
HVDOP(dop)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Time(location: Location) {
|
||||
if (location.time == 0L || !PreferenceUtils.isTrackingStarted(prefs)) {
|
||||
LocationValue("")
|
||||
} else {
|
||||
formatTime(location.time)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun formatTime(time: Long) {
|
||||
// SimpleDateFormat can only do 3 digits of fractional seconds (.SSS)
|
||||
val SDF_TIME_24_HOUR = "HH:mm:ss.SSS"
|
||||
val SDF_TIME_12_HOUR = "hh:mm:ss.SSS a"
|
||||
val SDF_DATE_24_HOUR = "HH:mm:ss.SSS MMM d, yyyy z"
|
||||
val SDF_DATE_12_HOUR = "hh:mm:ss.SSS a MMM d, yyyy z"
|
||||
|
||||
// See #117
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
val timeFormat = remember {
|
||||
SimpleDateFormat(
|
||||
if (DateFormat.is24HourFormat(Application.app.applicationContext)) SDF_TIME_24_HOUR else SDF_TIME_12_HOUR
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
val timeAndDateFormat = remember {
|
||||
SimpleDateFormat(
|
||||
if (DateFormat.is24HourFormat(Application.app.applicationContext)) SDF_DATE_24_HOUR else SDF_DATE_12_HOUR
|
||||
)
|
||||
}
|
||||
|
||||
if (LocalConfiguration.current.screenWidthDp > 450 || !DateTimeUtils.isTimeValid(time)) { // 450dp is a little larger than the width of a Samsung Galaxy S8+
|
||||
val dateAndTime = timeAndDateFormat.format(time).trimZeros()
|
||||
// Time and date
|
||||
if (DateTimeUtils.isTimeValid(time)) {
|
||||
LocationValue(dateAndTime)
|
||||
} else {
|
||||
ErrorTime(dateAndTime, time)
|
||||
}
|
||||
} else {
|
||||
// Time
|
||||
LocationValue(timeFormat.format(time).trimZeros())
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.trimZeros(): String {
|
||||
return this.replace(".000", "")
|
||||
.replace(",000", "")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TTFF(ttff: String) {
|
||||
LocationValue(ttff)
|
||||
}
|
||||
|
||||
/**
|
||||
* Horizontal and vertical location accuracies based on the provided location
|
||||
* @param location
|
||||
*/
|
||||
@Composable
|
||||
fun Accuracy(location: Location) {
|
||||
LocationValue(formatAccuracy(app, location, prefs))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NumSats(satelliteMetadata: SatelliteMetadata) {
|
||||
val fontStyle = if (gnssFilter(app, prefs).isNotEmpty()) {
|
||||
// Make text italic so it matches filter text
|
||||
FontStyle.Italic
|
||||
} else {
|
||||
FontStyle.Normal
|
||||
}
|
||||
LocationValue(
|
||||
formatNumSats(app, satelliteMetadata),
|
||||
fontStyle
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Bearing(location: Location) {
|
||||
LocationValue(formatBearing(app, location))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BearingAccuracy(location: Location) {
|
||||
LocationValue(formatBearingAccuracy(app, location))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HVDOP(dop: DilutionOfPrecision) {
|
||||
LocationValue(formatHvDOP(app, dop))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LabelColumn1() {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.wrapContentHeight()
|
||||
.wrapContentWidth()
|
||||
.padding(top = 5.dp, bottom = 5.dp, start = 5.dp, end = 5.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
LocationLabel(R.string.latitude_label)
|
||||
LocationLabel(R.string.longitude_label)
|
||||
LocationLabel(R.string.altitude_label)
|
||||
LocationLabel(R.string.altitude_msl_label)
|
||||
LocationLabel(R.string.speed_label)
|
||||
LocationLabel(R.string.speed_acc_label)
|
||||
LocationLabel(R.string.pdop_label)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LabelColumn2(location: Location) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.wrapContentHeight()
|
||||
.wrapContentWidth()
|
||||
.padding(top = 5.dp, bottom = 5.dp, end = 5.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
LocationLabel(R.string.fix_time_label)
|
||||
LocationLabel(R.string.ttff_label)
|
||||
LocationLabel(if (location.isVerticalAccuracySupported()) R.string.hor_and_vert_accuracy_label else R.string.accuracy_label)
|
||||
LocationLabel(R.string.num_sats_label)
|
||||
LocationLabel(R.string.bearing_label)
|
||||
LocationLabel(R.string.bearing_acc_label)
|
||||
LocationLabel(R.string.hvdop_label)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LocationLabel(@StringRes id: Int) {
|
||||
if (reduceSpacing()) {
|
||||
Text(
|
||||
text = stringResource(id),
|
||||
modifier = Modifier.padding(start = 4.dp, end = 4.dp),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 13.sp,
|
||||
letterSpacing = letterSpacing(),
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(id),
|
||||
modifier = Modifier.padding(start = 4.dp, end = 4.dp),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 13.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LocationValue(text: String, fontStyle: FontStyle = FontStyle.Normal) {
|
||||
if (reduceSpacing()) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.padding(end = 2.dp),
|
||||
fontSize = 13.sp,
|
||||
letterSpacing = letterSpacing(),
|
||||
fontStyle = fontStyle
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
fontSize = 13.sp,
|
||||
fontStyle = fontStyle
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ErrorTime(timeText: String, timeMs: Long) {
|
||||
var openDialog = remember { mutableStateOf(false) }
|
||||
// Red time box
|
||||
Box(
|
||||
Modifier
|
||||
.wrapContentHeight()
|
||||
.wrapContentWidth()
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(MaterialTheme.colors.error)
|
||||
.clickable {
|
||||
openDialog.value = true
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = timeText,
|
||||
modifier = Modifier.padding(start = 4.dp, end = 4.dp),
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colors.onError
|
||||
)
|
||||
}
|
||||
|
||||
// Alert Dialog
|
||||
val format = remember {
|
||||
SimpleDateFormat.getDateTimeInstance(
|
||||
java.text.DateFormat.LONG,
|
||||
java.text.DateFormat.LONG
|
||||
)
|
||||
}
|
||||
|
||||
if (openDialog.value) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
openDialog.value = false
|
||||
},
|
||||
title = {
|
||||
Text(stringResource(R.string.error_time_title))
|
||||
},
|
||||
text = {
|
||||
Column() {
|
||||
LinkifyText(
|
||||
text = Application.app.getString(
|
||||
R.string.error_time_message, format.format(timeMs),
|
||||
DateTimeUtils.NUM_DAYS_TIME_VALID
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
openDialog.value = false
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.ok))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun reduceSpacing(): Boolean {
|
||||
return LocalConfiguration.current.screenWidthDp < 365 // Galaxy S21+ is 384dp wide, Galaxy S8 is 326dp
|
||||
}
|
||||
|
||||
private fun letterSpacing(): TextUnit {
|
||||
// Reduce text spacing on narrow displays to make both columns fit
|
||||
return (-0.01).em
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun LockIcon(fixState: FixState) {
|
||||
var visible by remember { mutableStateOf(false) }
|
||||
visible = fixState == FixState.Acquired
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = scaleIn() + expandVertically(expandFrom = Alignment.CenterVertically),
|
||||
exit = scaleOut() + shrinkVertically(shrinkTowards = Alignment.CenterVertically)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_baseline_lock_24),
|
||||
contentDescription = stringResource(id = R.string.lock),
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
modifier = Modifier.padding(6.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyToClipboard(context: Context, location: Location) {
|
||||
if (location.latitude == 0.0 && location.longitude == 0.0) return
|
||||
val formattedLocation = LibUIUtils.formatLocationForDisplay(
|
||||
app, location, null, shareIncludeAltitude(app, prefs),
|
||||
null, null, null, coordinateFormat(app, prefs)
|
||||
)
|
||||
if (!TextUtils.isEmpty(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)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2008-2021 The Android Open Source Project,
|
||||
* 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.
|
||||
*/
|
||||
package com.android.gpstest.ui.status
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
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.library.ui.SignalInfoViewModel
|
||||
import com.android.gpstest.library.util.PreferenceUtil.darkTheme
|
||||
import com.android.gpstest.ui.theme.AppTheme
|
||||
import com.android.gpstest.util.UIUtils.showSortByDialog
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class StatusFragment : Fragment() {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val viewModel: SignalInfoViewModel by activityViewModels()
|
||||
return ComposeView(requireContext()).apply {
|
||||
// Dispose the Composition when the view's LifecycleOwner is destroyed
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
AppTheme(
|
||||
// TODO - add "system dark setting" as an option in preferences - Issue #277
|
||||
darkTheme = darkTheme(app, prefs)
|
||||
) {
|
||||
StatusScreen(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.status_menu, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val id = item.itemId
|
||||
if (id == R.id.sort_sats) {
|
||||
showSortByDialog(requireActivity())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.ui.status
|
||||
|
||||
import android.location.Location
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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.library.data.FixState
|
||||
import com.android.gpstest.library.model.DilutionOfPrecision
|
||||
import com.android.gpstest.library.model.GnssType
|
||||
import com.android.gpstest.library.model.SatelliteMetadata
|
||||
import com.android.gpstest.library.model.SatelliteStatus
|
||||
import com.android.gpstest.library.model.SbasType
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun StatusScreen(viewModel: SignalInfoViewModel) {
|
||||
//
|
||||
// Observe LiveData from ViewModel
|
||||
//
|
||||
val location: Location by viewModel.location.observeAsState(Location("default"))
|
||||
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 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())
|
||||
val allStatuses: List<SatelliteStatus> by viewModel.allStatuses.observeAsState(emptyList())
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Column {
|
||||
LocationCard(
|
||||
location,
|
||||
ttff,
|
||||
altitudeMsl,
|
||||
dop,
|
||||
satelliteMetadata,
|
||||
fixState)
|
||||
if (gnssFilter(app, prefs).isNotEmpty()) {
|
||||
Filter(allStatuses.size, satelliteMetadata) { PreferenceUtils.clearGnssFilter(app, prefs) }
|
||||
}
|
||||
GnssStatusCard(gnssStatuses)
|
||||
SbasStatusCard(sbasStatuses)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Filter(totalNumSignals: Int, satelliteMetadata: SatelliteMetadata, onClick: () -> Unit) {
|
||||
Row (
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(1.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.filter_signal_text,
|
||||
satelliteMetadata.numSignalsTotal,
|
||||
totalNumSignals
|
||||
),
|
||||
fontSize = 13.sp,
|
||||
fontStyle = FontStyle.Italic,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
val string = stringResource(id = R.string.filter_showall)
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
color = MaterialTheme.colors.primary,
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
) {
|
||||
append(string)
|
||||
}
|
||||
},
|
||||
fontStyle = FontStyle.Italic,
|
||||
fontSize = 13.sp,
|
||||
modifier = Modifier
|
||||
.padding(start = 2.dp)
|
||||
.clickable {
|
||||
onClick()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GnssStatusCard(satStatuses: List<SatelliteStatus>) {
|
||||
StatusCard(satStatuses, true)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SbasStatusCard(satStatuses: List<SatelliteStatus>) {
|
||||
StatusCard(satStatuses, false)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StatusCard(
|
||||
satStatuses: List<SatelliteStatus>,
|
||||
isGnss: Boolean,
|
||||
) {
|
||||
val modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(5.dp)
|
||||
|
||||
if (showList(isGnss, satStatuses)) {
|
||||
// Only scroll if we're showing satellites - we don't want "Not available" text to extend offscreen
|
||||
modifier.horizontalScroll(rememberScrollState())
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = modifier,
|
||||
elevation = 2.dp
|
||||
) {
|
||||
if (showList(isGnss, satStatuses)) {
|
||||
Column {
|
||||
StatusRowHeader(isGnss)
|
||||
satStatuses.forEach {
|
||||
StatusRow(it)
|
||||
}
|
||||
StatusRowFooter()
|
||||
}
|
||||
} else {
|
||||
NotAvailable(isGnss)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showList(isGnss: Boolean, satStatuses: List<SatelliteStatus>): Boolean {
|
||||
return isGnss ||
|
||||
(!isGnss && satStatuses.isNotEmpty())
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StatusRow(satelliteStatus: SatelliteStatus) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(start = 16.dp, end = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val small = Modifier.defaultMinSize(minWidth = 36.dp)
|
||||
val medium = Modifier.defaultMinSize(minWidth = dimensionResource(R.dimen.min_column_width_medium))
|
||||
val large = Modifier.defaultMinSize(minWidth = 50.dp)
|
||||
|
||||
Svid(satelliteStatus, small)
|
||||
Flag(satelliteStatus, large)
|
||||
CarrierFrequency(satelliteStatus, small)
|
||||
Cn0(satelliteStatus, medium)
|
||||
AEU(satelliteStatus, medium)
|
||||
Elevation(satelliteStatus, medium)
|
||||
Azimuth(satelliteStatus, medium)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Svid(satelliteStatus: SatelliteStatus, modifier: Modifier) {
|
||||
StatusValue(satelliteStatus.svid.toString(), modifier = modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Flag(satelliteStatus: SatelliteStatus, modifier: Modifier) {
|
||||
when (satelliteStatus.gnssType) {
|
||||
GnssType.NAVSTAR -> {
|
||||
FlagImage(R.drawable.ic_flag_usa, R.string.gps_content_description, modifier)
|
||||
}
|
||||
GnssType.GLONASS -> {
|
||||
FlagImage(R.drawable.ic_flag_russia, R.string.glonass_content_description, modifier)
|
||||
}
|
||||
GnssType.QZSS -> {
|
||||
FlagImage(R.drawable.ic_flag_japan, R.string.qzss_content_description, modifier)
|
||||
}
|
||||
GnssType.BEIDOU -> {
|
||||
FlagImage(R.drawable.ic_flag_china, R.string.beidou_content_description, modifier)
|
||||
}
|
||||
GnssType.GALILEO -> {
|
||||
FlagImage(R.drawable.ic_flag_european_union, R.string.galileo_content_description, modifier)
|
||||
}
|
||||
GnssType.IRNSS -> {
|
||||
FlagImage(R.drawable.ic_flag_india, R.string.irnss_content_description, modifier)
|
||||
}
|
||||
GnssType.SBAS -> SbasFlag(satelliteStatus, modifier)
|
||||
GnssType.UNKNOWN -> {
|
||||
Box(
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SbasFlag(status: SatelliteStatus, modifier: Modifier = Modifier) {
|
||||
when (status.sbasType) {
|
||||
SbasType.WAAS -> {
|
||||
FlagImage(R.drawable.ic_flag_usa, R.string.waas_content_description, modifier)
|
||||
}
|
||||
SbasType.EGNOS -> {
|
||||
FlagImage(R.drawable.ic_flag_european_union, R.string.egnos_content_description, modifier)
|
||||
}
|
||||
SbasType.GAGAN -> {
|
||||
FlagImage(R.drawable.ic_flag_india, R.string.gagan_content_description, modifier)
|
||||
}
|
||||
SbasType.MSAS -> {
|
||||
FlagImage(R.drawable.ic_flag_japan, R.string.msas_content_description, modifier)
|
||||
}
|
||||
SbasType.SDCM -> {
|
||||
FlagImage(R.drawable.ic_flag_russia, R.string.sdcm_content_description, modifier)
|
||||
}
|
||||
SbasType.SNAS -> {
|
||||
FlagImage(R.drawable.ic_flag_china, R.string.snas_content_description, modifier)
|
||||
}
|
||||
SbasType.SACCSA -> {
|
||||
FlagImage(R.drawable.ic_flag_icao, R.string.saccsa_content_description, modifier)
|
||||
}
|
||||
SbasType.SOUTHPAN -> {
|
||||
FlagImage(R.drawable.ic_flag_southpan, R.string.southpan_content_description, modifier)
|
||||
}
|
||||
SbasType.UNKNOWN -> {
|
||||
Box(
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FlagImage(@DrawableRes flagId: Int, @StringRes contentDescriptionId: Int, modifier: Modifier) {
|
||||
Box(
|
||||
modifier = modifier.padding(start = 3.dp, end = 3.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.border(BorderStroke(1.dp, Color.Black))
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = flagId),
|
||||
contentDescription = stringResource(id = contentDescriptionId),
|
||||
Modifier.padding(1.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CarrierFrequency(satelliteStatus: SatelliteStatus, modifier: Modifier) {
|
||||
if (satelliteStatus.hasCarrierFrequency) {
|
||||
val carrierLabel = CarrierFreqUtils.getCarrierFrequencyLabel(satelliteStatus)
|
||||
if (carrierLabel != CarrierFreqUtils.CF_UNKNOWN) {
|
||||
StatusValue(carrierLabel, modifier)
|
||||
} else {
|
||||
// Shrink the size so we can show raw number, convert Hz to MHz
|
||||
val carrierMhz = MathUtils.toMhz(satelliteStatus.carrierFrequencyHz)
|
||||
Text(
|
||||
text = String.format("%.3f", carrierMhz),
|
||||
modifier = modifier.padding(start = 3.dp, end = 2.dp),
|
||||
fontSize = 9.sp,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Box(
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Cn0(satelliteStatus: SatelliteStatus, modifier: Modifier) {
|
||||
if (satelliteStatus.cn0DbHz != SatelliteStatus.NO_DATA) {
|
||||
StatusValue(String.format("%.1f", satelliteStatus.cn0DbHz), modifier)
|
||||
} else {
|
||||
StatusValue("", modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AEU(satelliteStatus: SatelliteStatus, modifier: Modifier) {
|
||||
val flags = CharArray(3)
|
||||
flags[0] = if (satelliteStatus.hasAlmanac) 'A' else ' '
|
||||
flags[1] = if (satelliteStatus.hasEphemeris) 'E' else ' '
|
||||
flags[2] = if (satelliteStatus.usedInFix) 'U' else ' '
|
||||
StatusValue(String(flags), modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Elevation(satelliteStatus: SatelliteStatus, modifier: Modifier) {
|
||||
if (satelliteStatus.elevationDegrees != SatelliteStatus.NO_DATA) {
|
||||
StatusValue(
|
||||
stringResource(
|
||||
R.string.gps_elevation_column_value,
|
||||
satelliteStatus.elevationDegrees
|
||||
).trimZeros(),
|
||||
modifier
|
||||
)
|
||||
} else {
|
||||
StatusValue("", modifier)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.trimZeros(): String {
|
||||
return this.replace(".0", "")
|
||||
.replace(",0", "")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Azimuth(satelliteStatus: SatelliteStatus, modifier: Modifier) {
|
||||
if (satelliteStatus.azimuthDegrees != SatelliteStatus.NO_DATA) {
|
||||
StatusValue(
|
||||
stringResource(
|
||||
R.string.gps_azimuth_column_value,
|
||||
satelliteStatus.azimuthDegrees
|
||||
).trimZeros(),
|
||||
modifier
|
||||
)
|
||||
} else {
|
||||
StatusValue("", modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StatusRowHeader(isGnss: Boolean) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(top = 5.dp, start = 16.dp, end = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val small = Modifier.defaultMinSize(minWidth = dimensionResource(com.android.gpstest.library.R.dimen.min_column_width_small))
|
||||
val medium = Modifier.defaultMinSize(minWidth = dimensionResource(com.android.gpstest.library.R.dimen.min_column_width_medium))
|
||||
val large = Modifier.defaultMinSize(dimensionResource(com.android.gpstest.library.R.dimen.min_column_width_large))
|
||||
|
||||
StatusLabel(com.android.gpstest.library.R.string.id_column_label, small)
|
||||
if (isGnss) {
|
||||
StatusLabel(com.android.gpstest.library.R.string.gnss_flag_image_label, large)
|
||||
} else {
|
||||
StatusLabel(com.android.gpstest.library.R.string.sbas_flag_image_label, large)
|
||||
}
|
||||
StatusLabel(com.android.gpstest.library.R.string.cf_column_label, small)
|
||||
StatusLabel(com.android.gpstest.library.R.string.gps_cn0_column_label, medium)
|
||||
StatusLabel(com.android.gpstest.library.R.string.flags_aeu_column_label, medium)
|
||||
StatusLabel(com.android.gpstest.library.R.string.elevation_column_label, medium)
|
||||
StatusLabel(com.android.gpstest.library.R.string.azimuth_column_label, medium)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StatusLabel(@StringRes id: Int, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = stringResource(id),
|
||||
modifier = modifier.padding(start = 3.dp, end = 3.dp),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 13.sp,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StatusValue(text: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = modifier.padding(start = 3.dp, end = 3.dp),
|
||||
fontSize = 13.sp,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NotAvailable(isGnss: Boolean) {
|
||||
val message = if (isGnss) {
|
||||
stringResource(R.string.gnss_not_available)
|
||||
} else {
|
||||
stringResource(R.string.sbas_not_available)
|
||||
}
|
||||
NotAvailableText(message)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NotAvailableText(text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.padding(10.dp),
|
||||
fontSize = 13.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StatusRowFooter() {
|
||||
Spacer(modifier = Modifier.padding(bottom = 5.dp))
|
||||
}
|
||||
56
GPSTest/src/main/java/com/android/gpstest/ui/theme/Theme.kt
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.
|
||||
*/
|
||||
package com.android.gpstest.ui.theme
|
||||
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.darkColors
|
||||
import androidx.compose.material.lightColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
// TODO - For more theme colors and dark mode see:
|
||||
// * https://material.io/design/color/the-color-system.html#tools-for-picking-colors
|
||||
// * https://material.io/resources/color/#!/?view.left=0&view.right=1&primary.color=3f50b5&primary.text.color=ffffff
|
||||
// * https://material.io/blog/android-material-theme-color
|
||||
// * https://material.io/blog/android-dark-theme-tutorial
|
||||
// * https://material.io/blog/material-theme-builder
|
||||
// Issue #277
|
||||
|
||||
private val Purple500 = Color(0xFF3F51B5)
|
||||
private val Purple700 = Color(0xFF303F9F)
|
||||
|
||||
private val lightColors = lightColors(
|
||||
primary = Purple500,
|
||||
primaryVariant = Purple700
|
||||
)
|
||||
|
||||
private val darkColors = darkColors(
|
||||
primary = Purple500,
|
||||
primaryVariant = Purple700
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun AppTheme(
|
||||
darkTheme: Boolean,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
MaterialTheme(
|
||||
// isSystemInDarkTheme()
|
||||
colors = if (darkTheme) darkColors else lightColors,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
@@ -1,794 +0,0 @@
|
||||
/*
|
||||
* 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.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.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
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.os.Bundle;
|
||||
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;
|
||||
import android.widget.CheckBox;
|
||||
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.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.android.gpstest.Application;
|
||||
import com.android.gpstest.BuildConfig;
|
||||
import com.android.gpstest.DeviceInfoViewModel;
|
||||
import com.android.gpstest.R;
|
||||
import com.android.gpstest.dialog.ShareDialogFragment;
|
||||
import com.android.gpstest.io.CsvFileLogger;
|
||||
import com.android.gpstest.io.JsonFileLogger;
|
||||
import com.android.gpstest.model.GnssType;
|
||||
import com.android.gpstest.model.SbasType;
|
||||
import com.google.android.material.chip.Chip;
|
||||
|
||||
import java.io.File;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static android.content.Intent.createChooser;
|
||||
import static android.content.pm.PackageManager.GET_META_DATA;
|
||||
import static android.text.TextUtils.isEmpty;
|
||||
import static com.android.gpstest.util.IOUtils.replaceNavstar;
|
||||
import static com.android.gpstest.util.IOUtils.trimEnds;
|
||||
import static com.android.gpstest.view.GpsSkyView.MAX_VALUE_CN0;
|
||||
import static com.android.gpstest.view.GpsSkyView.MAX_VALUE_SNR;
|
||||
import static com.android.gpstest.view.GpsSkyView.MIN_VALUE_CN0;
|
||||
import static com.android.gpstest.view.GpsSkyView.MIN_VALUE_SNR;
|
||||
|
||||
/**
|
||||
* Utilities for processing user inteface elements
|
||||
*/
|
||||
|
||||
public class UIUtils {
|
||||
public static final String TAG = "UIUtils";
|
||||
|
||||
public static final String COORDINATE_LATITUDE = "lat";
|
||||
public static final String COORDINATE_LONGITUDE = "lon";
|
||||
|
||||
public static int PICKFILE_REQUEST_CODE = 101;
|
||||
|
||||
public static final int ANIMATION_DURATION_SHORT_MS = 200;
|
||||
public static final int ANIMATION_DURATION_MEDIUM_MS = 400;
|
||||
public static final int ANIMATION_DURATION_LONG_MS = 500;
|
||||
|
||||
/**
|
||||
* Formats a view so it is ignored for accessible access
|
||||
*/
|
||||
public static void setAccessibilityIgnore(View view) {
|
||||
view.setClickable(false);
|
||||
view.setFocusable(false);
|
||||
view.setContentDescription("");
|
||||
view.setImportantForAccessibility(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
|
||||
*/
|
||||
public static int dpToPixels(Context context, float dp) {
|
||||
// Get the screen's density scale
|
||||
final float scale = context.getResources().getDisplayMetrics().density;
|
||||
// Convert the dps to pixels, based on density scale
|
||||
return (int) (dp * scale + 0.5f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static boolean isWideEnoughForDate(Context context) {
|
||||
// 450dp is a little larger than the width of a Samsung Galaxy S8+
|
||||
final int WIDTH_THRESHOLD = dpToPixels(context, 450);
|
||||
return context.getResources().getDisplayMetrics().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
|
||||
*/
|
||||
public static boolean canManageDialog(Activity activity) {
|
||||
if (activity == null) {
|
||||
return false;
|
||||
}
|
||||
return !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
|
||||
*/
|
||||
public static boolean isFragmentAttached(Fragment f) {
|
||||
return f.getActivity() != null && f.isAdded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable description of the time-to-first-fix, such as "38 sec"
|
||||
*
|
||||
* @param ttff time-to-first fix, in milliseconds
|
||||
* @return a human-readable description of the time-to-first-fix, such as "38 sec"
|
||||
*/
|
||||
public static String getTtffString(int ttff) {
|
||||
if (ttff == 0) {
|
||||
return "";
|
||||
} else {
|
||||
return TimeUnit.MILLISECONDS.toSeconds(ttff) + " sec";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided SNR values to a left margin value (pixels) for the avg SNR indicator ImageViews in gps_sky_signal
|
||||
* Left margin range for the SNR 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 snr signal-to-noise ratio of the satellite in dB (from GpsSatellite)
|
||||
* @return left margin value in pixels for the SNR indicator ImageViews
|
||||
*/
|
||||
public static int snrToIndicatorLeftMarginPx(float snr, int minIndicatorMarginPx, int maxIndicatorMarginPx) {
|
||||
return (int) MathUtils.mapToRange(snr, MIN_VALUE_SNR, MAX_VALUE_SNR, minIndicatorMarginPx, maxIndicatorMarginPx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided SNR values to a left margin value (pixels) for the avg SNR TextViews in gps_sky_signal
|
||||
* Left margin range for the SNR 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 snr signal-to-noise ratio of the satellite in dB (from GpsSatellite)
|
||||
* @return left margin value in dp for the SNR TextViews
|
||||
*/
|
||||
public static int snrToTextViewLeftMarginPx(float snr, int minTextViewMarginPx, int maxTextViewMarginPx) {
|
||||
return (int) MathUtils.mapToRange(snr, MIN_VALUE_SNR, MAX_VALUE_SNR, minTextViewMarginPx, maxTextViewMarginPx);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static int cn0ToIndicatorLeftMarginPx(float cn0, int minIndicatorMarginPx, int maxIndicatorMarginPx) {
|
||||
return (int) MathUtils.mapToRange(cn0, MIN_VALUE_CN0, MAX_VALUE_CN0, minIndicatorMarginPx, maxIndicatorMarginPx);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static int cn0ToTextViewLeftMarginPx(float cn0, int minTextViewMarginPx, int maxTextViewMarginPx) {
|
||||
return (int) MathUtils.mapToRange(cn0, MIN_VALUE_CN0, MAX_VALUE_CN0, minTextViewMarginPx, maxTextViewMarginPx);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static void setMargins(View v, int l, int t, int r, int b) {
|
||||
ViewGroup.MarginLayoutParams p = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
|
||||
p.setMargins(l, t, r, b);
|
||||
v.setLayoutParams(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens email apps based on the given email address
|
||||
* @param email address
|
||||
* @param location string that shows the current location
|
||||
* @param deviceInfoViewModel view model that contains state of GNSS
|
||||
*/
|
||||
public static void sendEmail(Context context, String email, String location, DeviceInfoViewModel deviceInfoViewModel) {
|
||||
LocationManager locationManager = (LocationManager) Application.get().getSystemService(Context.LOCATION_SERVICE);
|
||||
PackageManager pm = context.getPackageManager();
|
||||
PackageInfo appInfo;
|
||||
|
||||
StringBuilder body = new StringBuilder();
|
||||
body.append(context.getString(R.string.feedback_body));
|
||||
|
||||
String versionName = "";
|
||||
int versionCode = 0;
|
||||
|
||||
try {
|
||||
appInfo = pm.getPackageInfo(context.getPackageName(),
|
||||
PackageManager.GET_META_DATA);
|
||||
versionName = appInfo.versionName;
|
||||
versionCode = appInfo.versionCode;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
// Leave version as empty string
|
||||
}
|
||||
|
||||
// App version
|
||||
body.append("App version: v")
|
||||
.append(versionName)
|
||||
.append(" (")
|
||||
.append(versionCode)
|
||||
.append("-" + BuildConfig.FLAVOR + ")\n");
|
||||
|
||||
// Device properties
|
||||
body.append("Model: " + Build.MODEL + "\n");
|
||||
body.append("Android version: " + Build.VERSION.RELEASE + " / " + Build.VERSION.SDK_INT + "\n");
|
||||
|
||||
if (!TextUtils.isEmpty(location)) {
|
||||
body.append("Location: " + location + "\n");
|
||||
}
|
||||
|
||||
body.append("GNSS HW year: " + IOUtils.getGnssHardwareYear() + "\n");
|
||||
if (!IOUtils.getGnssHardwareModelName().trim().isEmpty()) {
|
||||
body.append("GNSS HW name: " + IOUtils.getGnssHardwareModelName() + "\n");
|
||||
}
|
||||
|
||||
// Raw GNSS measurement capability
|
||||
int capability = Application.getPrefs().getInt(Application.get().getString(R.string.capability_key_raw_measurements), PreferenceUtils.CAPABILITY_UNKNOWN);
|
||||
if (capability != PreferenceUtils.CAPABILITY_UNKNOWN) {
|
||||
body.append(Application.get().getString(R.string.capability_title_raw_measurements, PreferenceUtils.getCapabilityDescription(capability)));
|
||||
}
|
||||
|
||||
// Navigation messages capability
|
||||
capability = Application.getPrefs().getInt(Application.get().getString(R.string.capability_key_nav_messages), PreferenceUtils.CAPABILITY_UNKNOWN);
|
||||
if (capability != PreferenceUtils.CAPABILITY_UNKNOWN) {
|
||||
body.append(Application.get().getString(R.string.capability_title_nav_messages, PreferenceUtils.getCapabilityDescription(capability)));
|
||||
}
|
||||
|
||||
// NMEA capability
|
||||
capability = Application.getPrefs().getInt(Application.get().getString(R.string.capability_key_nmea), PreferenceUtils.CAPABILITY_UNKNOWN);
|
||||
if (capability != PreferenceUtils.CAPABILITY_UNKNOWN) {
|
||||
body.append(Application.get().getString(R.string.capability_title_nmea, PreferenceUtils.getCapabilityDescription(capability)));
|
||||
}
|
||||
|
||||
// Inject PSDS capability
|
||||
capability = Application.getPrefs().getInt(Application.get().getString(R.string.capability_key_inject_psds), PreferenceUtils.CAPABILITY_UNKNOWN);
|
||||
if (capability != PreferenceUtils.CAPABILITY_UNKNOWN) {
|
||||
body.append(Application.get().getString(R.string.capability_title_inject_psds, PreferenceUtils.getCapabilityDescription(capability)));
|
||||
}
|
||||
|
||||
// Inject time capability
|
||||
capability = Application.getPrefs().getInt(Application.get().getString(R.string.capability_key_inject_time), PreferenceUtils.CAPABILITY_UNKNOWN);
|
||||
if (capability != PreferenceUtils.CAPABILITY_UNKNOWN) {
|
||||
body.append(Application.get().getString(R.string.capability_title_inject_time, PreferenceUtils.getCapabilityDescription(capability)));
|
||||
}
|
||||
|
||||
// Delete assist capability
|
||||
capability = Application.getPrefs().getInt(Application.get().getString(R.string.capability_key_delete_assist), PreferenceUtils.CAPABILITY_UNKNOWN);
|
||||
if (capability != PreferenceUtils.CAPABILITY_UNKNOWN) {
|
||||
body.append(Application.get().getString(R.string.capability_title_delete_assist, PreferenceUtils.getCapabilityDescription(capability)));
|
||||
}
|
||||
|
||||
// Got fix
|
||||
body.append(Application.get().getString(R.string.capability_title_got_fix, location != null && deviceInfoViewModel.gotFirstFix()));
|
||||
|
||||
// We need a fix to determine these attributes reliably
|
||||
if (location != null && deviceInfoViewModel.gotFirstFix()) {
|
||||
// Dual frequency
|
||||
body.append(Application.get().getString(R.string.capability_title_dual_frequency, PreferenceUtils.getCapabilityDescription(deviceInfoViewModel.isNonPrimaryCarrierFreqInView())));
|
||||
// Supported GNSS
|
||||
List<GnssType> gnss = new ArrayList<>(deviceInfoViewModel.getSupportedGnss());
|
||||
Collections.sort(gnss);
|
||||
body.append(Application.get().getString(R.string.capability_title_supported_gnss, trimEnds(replaceNavstar(gnss.toString()))));
|
||||
// GNSS CF
|
||||
List<String> gnssCfs = new ArrayList<>(deviceInfoViewModel.getSupportedGnssCfs());
|
||||
if (!gnssCfs.isEmpty()) {
|
||||
Collections.sort(gnssCfs);
|
||||
body.append(Application.get().getString(R.string.capability_title_gnss_cf, trimEnds(gnssCfs.toString())));
|
||||
}
|
||||
// Supported SBAS
|
||||
List<SbasType> sbas = new ArrayList<>(deviceInfoViewModel.getSupportedSbas());
|
||||
if (!sbas.isEmpty()) {
|
||||
Collections.sort(sbas);
|
||||
body.append(Application.get().getString(R.string.capability_title_supported_sbas, trimEnds(sbas.toString())));
|
||||
}
|
||||
// SBAS CF
|
||||
List<String> sbasCfs = new ArrayList<>(deviceInfoViewModel.getSupportedSbasCfs());
|
||||
if (!sbasCfs.isEmpty()) {
|
||||
Collections.sort(sbasCfs);
|
||||
body.append(Application.get().getString(R.string.capability_title_sbas_cf, trimEnds(sbasCfs.toString())));
|
||||
}
|
||||
// Accumulated delta range
|
||||
body.append(Application.get().getString(R.string.capability_title_accumulated_delta_range, PreferenceUtils.getCapabilityDescription(Application.getPrefs().getInt(Application.get().getString(R.string.capability_key_measurement_delta_range), PreferenceUtils.CAPABILITY_UNKNOWN))));
|
||||
// Automatic gain control
|
||||
body.append(Application.get().getString(R.string.capability_title_automatic_gain_control, PreferenceUtils.getCapabilityDescription(Application.getPrefs().getInt(Application.get().getString(R.string.capability_key_measurement_automatic_gain_control), PreferenceUtils.CAPABILITY_UNKNOWN))));
|
||||
}
|
||||
|
||||
// GNSS Antenna Info
|
||||
String gnssAntennaInfo = Application.get().getString(R.string.capability_title_gnss_antenna_info, PreferenceUtils.getCapabilityDescription(SatelliteUtils.isGnssAntennaInfoSupported(locationManager)));
|
||||
body.append(gnssAntennaInfo);
|
||||
if (gnssAntennaInfo.equals(Application.get().getString(R.string.capability_value_supported))) {
|
||||
body.append(Application.get().getString(R.string.capability_title_num_antennas, PreferenceUtils.getInt(Application.get().getString(R.string.capability_key_num_antenna), -1)));
|
||||
body.append(Application.get().getString(R.string.capability_title_antenna_cfs, PreferenceUtils.getString(Application.get().getString(R.string.capability_key_antenna_cf))));
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(BuildUtils.getPlayServicesVersion())) {
|
||||
body.append("\n" + BuildUtils.getPlayServicesVersion());
|
||||
}
|
||||
|
||||
body.append("\n\n\n");
|
||||
|
||||
Intent send = new Intent(Intent.ACTION_SENDTO);
|
||||
send.setData(Uri.parse("mailto:"));
|
||||
send.putExtra(Intent.EXTRA_EMAIL, new String[]{email});
|
||||
|
||||
String subject = context.getString(R.string.feedback_subject);
|
||||
|
||||
send.putExtra(Intent.EXTRA_SUBJECT, subject);
|
||||
send.putExtra(Intent.EXTRA_TEXT, body.toString());
|
||||
|
||||
try {
|
||||
context.startActivity(createChooser(send, subject));
|
||||
} catch (ActivityNotFoundException e) {
|
||||
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
|
||||
* @return the provided latitude or longitude value in Degrees Minutes Seconds (DMS) format
|
||||
*/
|
||||
public static String getDMSFromLocation(Context context, double coordinate, String latOrLon) {
|
||||
BigDecimal loc = new BigDecimal(coordinate);
|
||||
BigDecimal degrees = loc.setScale(0, RoundingMode.DOWN);
|
||||
BigDecimal minTemp = loc.subtract(degrees).multiply((new BigDecimal(60))).abs();
|
||||
BigDecimal minutes = minTemp.setScale(0, RoundingMode.DOWN);
|
||||
BigDecimal seconds = minTemp.subtract(minutes).multiply(new BigDecimal(60)).setScale(2, RoundingMode.HALF_UP);
|
||||
|
||||
String hemisphere;
|
||||
int output_string;
|
||||
if (latOrLon.equals(UIUtils.COORDINATE_LATITUDE)) {
|
||||
hemisphere = (coordinate < 0 ? "S" : "N");
|
||||
output_string = R.string.gps_lat_dms_value;
|
||||
} else {
|
||||
hemisphere = (coordinate < 0 ? "W" : "E");
|
||||
output_string = R.string.gps_lon_dms_value;
|
||||
}
|
||||
|
||||
return context.getString(output_string, hemisphere, degrees.abs().intValue(), minutes.intValue(), seconds.floatValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provided latitude or longitude value in Decimal Degree Minutes (DDM) format
|
||||
*
|
||||
* @param coordinate latitude or longitude to convert to DDM format
|
||||
* @param latOrLon lat or lon to format hemisphere
|
||||
* @return the provided latitude or longitude value in Decimal Degree Minutes (DDM) format
|
||||
*/
|
||||
public static String getDDMFromLocation(Context context, double coordinate, String latOrLon) {
|
||||
BigDecimal loc = new BigDecimal(coordinate);
|
||||
BigDecimal degrees = loc.setScale(0, RoundingMode.DOWN);
|
||||
BigDecimal minutes = loc.subtract(degrees).multiply((new BigDecimal(60))).abs().setScale(3, RoundingMode.HALF_UP);
|
||||
String hemisphere;
|
||||
int output_string;
|
||||
if (latOrLon.equals(COORDINATE_LATITUDE)) {
|
||||
hemisphere = (coordinate < 0 ? "S" : "N");
|
||||
output_string = R.string.gps_lat_ddm_value;
|
||||
} else {
|
||||
hemisphere = (coordinate < 0 ? "W" : "E");
|
||||
output_string = R.string.gps_lon_ddm_value;
|
||||
}
|
||||
return context.getString(output_string, hemisphere, degrees.abs().intValue(), minutes.floatValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static double toFeet(double meters) {
|
||||
return meters * 1000d / 25.4d / 12d;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static float toKilometersPerHour(float metersPerSecond) {
|
||||
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
|
||||
*/
|
||||
public static float toMilesPerHour(float metersPerSecond) {
|
||||
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
|
||||
*/
|
||||
public static void setVerticalBias(View view, float bias) {
|
||||
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) view.getLayoutParams();
|
||||
params.verticalBias = bias;
|
||||
view.setLayoutParams(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
|
||||
* @param activity
|
||||
* @param lat latitude to validate
|
||||
* @param lon longitude to validate
|
||||
* @param alt altitude to validate
|
||||
* @return true if the latitude, longitude, and latitude are valid, false if any of them are not
|
||||
*/
|
||||
public static boolean isValidLocationWithErrorDialog(AppCompatActivity activity, String lat, String lon, String alt) {
|
||||
String dialogTitle = Application.get().getString(R.string.ground_truth_invalid_location_title);
|
||||
String dialogMessage;
|
||||
|
||||
if (!LocationUtils.isValidLatitude(lat)) {
|
||||
dialogMessage = Application.get().getString(R.string.ground_truth_invalid_lat);
|
||||
UIUtils.showLocationErrorDialog(activity, dialogTitle, dialogMessage);
|
||||
return false;
|
||||
}
|
||||
if (!LocationUtils.isValidLongitude(lon)) {
|
||||
dialogMessage = Application.get().getString(R.string.ground_truth_invalid_long);
|
||||
UIUtils.showLocationErrorDialog(activity, dialogTitle, dialogMessage);
|
||||
return false;
|
||||
}
|
||||
if (!isEmpty(alt) && !LocationUtils.isValidAltitude(alt)) {
|
||||
dialogMessage = Application.get().getString(R.string.ground_truth_invalid_alt);
|
||||
UIUtils.showLocationErrorDialog(activity, dialogTitle, dialogMessage);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an error dialog for an incorrectly entered latitude, longitude, or altitude
|
||||
* @param activity
|
||||
* @param title title of the error dialog
|
||||
* @param message message body of the error dialog
|
||||
*/
|
||||
private static void showLocationErrorDialog(AppCompatActivity activity, String title, String message) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.ok, (dialog, id) -> { })
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
public static Dialog createQrCodeDialog(AppCompatActivity activity) {
|
||||
View view = activity.getLayoutInflater().inflate(R.layout.qr_code_instructions, null);
|
||||
CheckBox neverShowDialog = view.findViewById(R.id.qr_code_never_show_again);
|
||||
|
||||
neverShowDialog.setOnCheckedChangeListener((compoundButton, isChecked) -> {
|
||||
// Save the preference
|
||||
PreferenceUtils.saveBoolean(Application.get().getString(R.string.pref_key_never_show_qr_code_instructions), isChecked);
|
||||
});
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.qr_code_instructions_title)
|
||||
.setCancelable(false)
|
||||
.setView(view)
|
||||
.setPositiveButton(R.string.ok,
|
||||
(dialog, which) -> IOUtils.openQrCodeReader(activity)
|
||||
).setNegativeButton(R.string.not_now,
|
||||
(dialog, which) -> {
|
||||
// No op
|
||||
}
|
||||
);
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a dialog for sharing location and files
|
||||
*
|
||||
* @param activity
|
||||
* @param location
|
||||
* @param loggingEnabled true if logging is enabled, false if it is not
|
||||
* @param csvFileLogger the file logger being used to log files
|
||||
* @param alternateFileUri The URI for a file if a file other than the one current used by the FileLogger should be used (e.g., one previously picked from the folder browse button), or null if no alternate file is chosen and the file from the file logger should be shared.
|
||||
* @return a dialog for sharing location and files
|
||||
*/
|
||||
public static void showShareFragmentDialog(AppCompatActivity activity, final Location location,
|
||||
boolean loggingEnabled, CsvFileLogger csvFileLogger,
|
||||
JsonFileLogger jsonFileLogger, Uri alternateFileUri) {
|
||||
ArrayList<File> files = new ArrayList<>(2);
|
||||
if (csvFileLogger != null && csvFileLogger.getFile() != null) {
|
||||
files.add(csvFileLogger.getFile());
|
||||
}
|
||||
if (jsonFileLogger != null && jsonFileLogger.getFile() != null) {
|
||||
files.add(jsonFileLogger.getFile());
|
||||
}
|
||||
|
||||
FragmentManager fm = activity.getSupportFragmentManager();
|
||||
final ShareDialogFragment dialog = new ShareDialogFragment();
|
||||
final ShareDialogFragment.Listener shareListener = new ShareDialogFragment.Listener() {
|
||||
@Override
|
||||
public void onLogFileSent() {
|
||||
if (csvFileLogger != null) {
|
||||
csvFileLogger.close();
|
||||
}
|
||||
if (jsonFileLogger != null) {
|
||||
jsonFileLogger.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFileBrowse() {
|
||||
if (dialog != null) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
};
|
||||
dialog.setListener(shareListener);
|
||||
dialog.setArguments(createBundleForShareDialog(location, loggingEnabled, files, alternateFileUri));
|
||||
dialog.show(fm, ShareDialogFragment.Companion.getTAG());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a bundle out of the provided variables for passing between fragments
|
||||
* @param location
|
||||
* @param loggingEnabled
|
||||
* @param files
|
||||
* @param alternateFileUri
|
||||
* @return a bundle out of the provided variables for passing between fragments
|
||||
*/
|
||||
private static Bundle createBundleForShareDialog(final Location location,
|
||||
boolean loggingEnabled, ArrayList<File> files,
|
||||
Uri alternateFileUri) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(ShareDialogFragment.Companion.getKEY_LOCATION(), location);
|
||||
bundle.putBoolean(ShareDialogFragment.Companion.getKEY_LOGGING_ENABLED(), loggingEnabled);
|
||||
bundle.putSerializable(ShareDialogFragment.Companion.getKEY_LOG_FILES(), files);
|
||||
bundle.putParcelable(ShareDialogFragment.Companion.getKEY_ALTERNATE_FILE_URI(), alternateFileUri);
|
||||
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
|
||||
*/
|
||||
public static String formatLocationForDisplay(Location location, TextView textView, boolean includeAltitude, Chip chipDecimalDegrees, Chip chipDMS, Chip chipDegreesDecimalMin, String coordinateFormat) {
|
||||
String formattedLocation = "";
|
||||
switch (coordinateFormat) {
|
||||
// Constants below must match string values in do_not_translate.xml
|
||||
case "dd":
|
||||
// Decimal degrees
|
||||
formattedLocation = IOUtils.createLocationShare(location, includeAltitude);
|
||||
if (chipDecimalDegrees != null) {
|
||||
chipDecimalDegrees.setChecked(true);
|
||||
}
|
||||
break;
|
||||
case "dms":
|
||||
// Degrees minutes seconds
|
||||
if (location != null) {
|
||||
formattedLocation = IOUtils.createLocationShare(UIUtils.getDMSFromLocation(Application.get(), location.getLatitude(), UIUtils.COORDINATE_LATITUDE),
|
||||
UIUtils.getDMSFromLocation(Application.get(), location.getLongitude(), UIUtils.COORDINATE_LONGITUDE),
|
||||
(location.hasAltitude() && includeAltitude) ? Double.toString(location.getAltitude()) : null);
|
||||
}
|
||||
if (chipDMS != null) {
|
||||
chipDMS.setChecked(true);
|
||||
}
|
||||
break;
|
||||
case "ddm":
|
||||
// Degrees decimal minutes
|
||||
if (location != null) {
|
||||
formattedLocation = IOUtils.createLocationShare(UIUtils.getDDMFromLocation(Application.get(), location.getLatitude(), UIUtils.COORDINATE_LATITUDE),
|
||||
UIUtils.getDDMFromLocation(Application.get(), location.getLongitude(), UIUtils.COORDINATE_LONGITUDE),
|
||||
(location.hasAltitude() && includeAltitude) ? Double.toString(location.getAltitude()) : null);
|
||||
}
|
||||
if (chipDegreesDecimalMin != null) {
|
||||
chipDegreesDecimalMin.setChecked(true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Decimal degrees
|
||||
formattedLocation = IOUtils.createLocationShare(location, includeAltitude);
|
||||
if (chipDecimalDegrees != null) {
|
||||
chipDecimalDegrees.setChecked(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (textView != null) {
|
||||
textView.setText(formattedLocation);
|
||||
}
|
||||
return formattedLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the activity title so the locale is updated
|
||||
*
|
||||
* @param a the activity to reset the title for
|
||||
*/
|
||||
public static void resetActivityTitle(Activity a) {
|
||||
try {
|
||||
ActivityInfo info = a.getPackageManager().getActivityInfo(a.getComponentName(), GET_META_DATA);
|
||||
if (info.labelRes != 0) {
|
||||
a.setTitle(info.labelRes);
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
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
|
||||
*/
|
||||
public static boolean isLargeScreen(Context context) {
|
||||
return (context.getResources().getConfiguration().screenLayout
|
||||
& 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
|
||||
*/
|
||||
public static String getGnssDisplayName(Context context, GnssType gnssType) {
|
||||
switch(gnssType) {
|
||||
case NAVSTAR:
|
||||
return context.getResources().getString(R.string.sky_legend_shape_navstar);
|
||||
case GALILEO:
|
||||
return context.getResources().getString(R.string.sky_legend_shape_galileo);
|
||||
case GLONASS:
|
||||
return context.getResources().getString(R.string.sky_legend_shape_glonass);
|
||||
case BEIDOU:
|
||||
return context.getResources().getString(R.string.sky_legend_shape_beidou);
|
||||
case QZSS:
|
||||
return context.getResources().getString(R.string.sky_legend_shape_qzss);
|
||||
case IRNSS:
|
||||
return context.getResources().getString(R.string.sky_legend_shape_irnss);
|
||||
case SBAS:
|
||||
return context.getResources().getString(R.string.sbas);
|
||||
case UNKNOWN:
|
||||
default:
|
||||
return context.getResources().getString(R.string.unknown);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setClickableSpan(TextView v, ClickableSpan span) {
|
||||
Spannable text = (Spannable) v.getText();
|
||||
text.setSpan(span, 0, text.length(), 0);
|
||||
v.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
public static void removeAllClickableSpans(TextView v) {
|
||||
Spannable text = (Spannable) v.getText();
|
||||
ClickableSpan[] spans = text.getSpans(0, text.length(), ClickableSpan.class);
|
||||
for (ClickableSpan cs : spans) {
|
||||
text.removeSpan(cs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a view using animation
|
||||
*
|
||||
* @param v View to show
|
||||
* @param animationDuration duration of animation
|
||||
*/
|
||||
public static void showViewWithAnimation(final View v, int animationDuration) {
|
||||
if (v.getVisibility() == View.VISIBLE && v.getAlpha() == 1) {
|
||||
// View is already visible and not transparent, return without doing anything
|
||||
return;
|
||||
}
|
||||
|
||||
v.clearAnimation();
|
||||
v.animate().cancel();
|
||||
|
||||
if (v.getVisibility() != View.VISIBLE) {
|
||||
// Set the content view to 0% opacity but visible, so that it is visible
|
||||
// (but fully transparent) during the animation.
|
||||
v.setAlpha(0f);
|
||||
v.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// Animate the content view to 100% opacity, and clear any animation listener set on the view.
|
||||
v.animate()
|
||||
.alpha(1f)
|
||||
.setDuration(animationDuration)
|
||||
.setListener(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides a view using animation
|
||||
*
|
||||
* @param v View to hide
|
||||
* @param animationDuration duration of animation
|
||||
*/
|
||||
public static void hideViewWithAnimation(final View v, int animationDuration) {
|
||||
if (v.getVisibility() == 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)
|
||||
.setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
v.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
270
GPSTest/src/main/java/com/android/gpstest/util/UIUtils.kt
Normal file
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* 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.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.location.Location
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.widget.CheckBox
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
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.R
|
||||
import com.android.gpstest.io.CsvFileLogger
|
||||
import com.android.gpstest.io.JsonFileLogger
|
||||
import com.android.gpstest.library.model.GnssType
|
||||
import com.android.gpstest.library.util.IOUtils
|
||||
import com.android.gpstest.library.util.LibUIUtils
|
||||
import com.android.gpstest.library.util.LocationUtils
|
||||
import com.android.gpstest.library.util.PreferenceUtils
|
||||
import com.android.gpstest.ui.GnssFilterDialog
|
||||
import com.android.gpstest.ui.HelpActivity
|
||||
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 java.io.File
|
||||
|
||||
/**
|
||||
* Utilities for processing user inteface elements
|
||||
*/
|
||||
internal object UIUtils {
|
||||
/**
|
||||
* 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
|
||||
* @param activity
|
||||
* @param lat latitude to validate
|
||||
* @param lon longitude to validate
|
||||
* @param alt altitude to validate
|
||||
* @return true if the latitude, longitude, and latitude are valid, false if any of them are not
|
||||
*/
|
||||
@JvmStatic
|
||||
fun isValidLocationWithErrorDialog(
|
||||
activity: AppCompatActivity,
|
||||
lat: String?,
|
||||
lon: String?,
|
||||
alt: String?
|
||||
): Boolean {
|
||||
val dialogTitle = app.getString(R.string.ground_truth_invalid_location_title)
|
||||
val dialogMessage: String
|
||||
if (!LocationUtils.isValidLatitude(lat)) {
|
||||
dialogMessage = app.getString(R.string.ground_truth_invalid_lat)
|
||||
showLocationErrorDialog(activity, dialogTitle, dialogMessage)
|
||||
return false
|
||||
}
|
||||
if (!LocationUtils.isValidLongitude(lon)) {
|
||||
dialogMessage = app.getString(R.string.ground_truth_invalid_long)
|
||||
showLocationErrorDialog(activity, dialogTitle, dialogMessage)
|
||||
return false
|
||||
}
|
||||
if (!TextUtils.isEmpty(alt) && !LocationUtils.isValidAltitude(alt)) {
|
||||
dialogMessage = app.getString(R.string.ground_truth_invalid_alt)
|
||||
showLocationErrorDialog(activity, dialogTitle, dialogMessage)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an error dialog for an incorrectly entered latitude, longitude, or altitude
|
||||
* @param activity
|
||||
* @param title title of the error dialog
|
||||
* @param message message body of the error dialog
|
||||
*/
|
||||
private fun showLocationErrorDialog(
|
||||
activity: AppCompatActivity,
|
||||
title: String,
|
||||
message: String
|
||||
) {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.ok) { dialog: DialogInterface?, id: Int -> }
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun createQrCodeDialog(activity: AppCompatActivity): Dialog {
|
||||
val view = activity.layoutInflater.inflate(R.layout.qr_code_instructions, null)
|
||||
val neverShowDialog = view.findViewById<CheckBox>(R.id.qr_code_never_show_again)
|
||||
neverShowDialog.setOnCheckedChangeListener { compoundButton: CompoundButton?, isChecked: Boolean ->
|
||||
// Save the preference
|
||||
PreferenceUtils.saveBoolean(
|
||||
app.getString(R.string.pref_key_never_show_qr_code_instructions),
|
||||
isChecked,
|
||||
Application.prefs
|
||||
)
|
||||
}
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.qr_code_instructions_title)
|
||||
.setCancelable(false)
|
||||
.setView(view)
|
||||
.setPositiveButton(
|
||||
R.string.ok
|
||||
) { dialog: DialogInterface?, which: Int -> IOUtils.openQrCodeReader(activity) }
|
||||
.setNegativeButton(
|
||||
R.string.not_now
|
||||
) { dialog: DialogInterface?, which: Int -> }
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a dialog for sharing location and files
|
||||
*
|
||||
* @param activity
|
||||
* @param location
|
||||
* @param loggingEnabled true if logging is enabled, false if it is not
|
||||
* @param csvFileLogger the file logger being used to log files
|
||||
* @param alternateFileUri The URI for a file if a file other than the one current used by the FileLogger should be used (e.g., one previously picked from the folder browse button), or null if no alternate file is chosen and the file from the file logger should be shared.
|
||||
* @return a dialog for sharing location and files
|
||||
*/
|
||||
fun showShareFragmentDialog(
|
||||
activity: AppCompatActivity, location: Location?,
|
||||
loggingEnabled: Boolean, csvFileLogger: CsvFileLogger?,
|
||||
jsonFileLogger: JsonFileLogger?, alternateFileUri: Uri?
|
||||
) {
|
||||
val files = ArrayList<File>(2)
|
||||
if (csvFileLogger != null && csvFileLogger.file != null) {
|
||||
files.add(csvFileLogger.file)
|
||||
}
|
||||
if (jsonFileLogger != null && jsonFileLogger.file != null) {
|
||||
files.add(jsonFileLogger.file)
|
||||
}
|
||||
val fm = activity.supportFragmentManager
|
||||
val dialog = ShareDialogFragment()
|
||||
val shareListener: ShareDialogFragment.Listener = object : ShareDialogFragment.Listener {
|
||||
override fun onLogFileSent() {
|
||||
csvFileLogger?.close()
|
||||
jsonFileLogger?.close()
|
||||
}
|
||||
|
||||
override fun onFileBrowse() {
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
dialog.setListener(shareListener)
|
||||
dialog.arguments =
|
||||
createBundleForShareDialog(location, loggingEnabled, files, alternateFileUri)
|
||||
dialog.show(fm, ShareDialogFragment.TAG)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a bundle out of the provided variables for passing between fragments
|
||||
* @param location
|
||||
* @param loggingEnabled
|
||||
* @param files
|
||||
* @param alternateFileUri
|
||||
* @return a bundle out of the provided variables for passing between fragments
|
||||
*/
|
||||
private fun createBundleForShareDialog(
|
||||
location: Location?,
|
||||
loggingEnabled: Boolean,
|
||||
files: ArrayList<File>,
|
||||
alternateFileUri: Uri?
|
||||
): Bundle {
|
||||
val bundle = Bundle()
|
||||
bundle.putParcelable(KEY_LOCATION, location)
|
||||
bundle.putBoolean(KEY_LOGGING_ENABLED, loggingEnabled)
|
||||
bundle.putSerializable(KEY_LOG_FILES, files)
|
||||
bundle.putParcelable(KEY_ALTERNATE_FILE_URI, alternateFileUri)
|
||||
return bundle
|
||||
}
|
||||
|
||||
fun createHelpDialog(activity: Activity): Dialog {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setTitle(R.string.title_help)
|
||||
val options = R.array.main_help_options
|
||||
builder.setItems(
|
||||
options
|
||||
) { dialog: DialogInterface?, which: Int ->
|
||||
when (which) {
|
||||
0 -> activity.showDialog(
|
||||
LibUIUtils.WHATSNEW_DIALOG
|
||||
)
|
||||
1 -> activity.startActivity(Intent(activity, HelpActivity::class.java))
|
||||
}
|
||||
}
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
fun createWhatsNewDialog(activity: Activity): Dialog {
|
||||
val textView = activity.layoutInflater.inflate(R.layout.whats_new_dialog, null) as TextView
|
||||
textView.setText(R.string.main_help_whatsnew)
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setTitle(R.string.main_help_whatsnew_title)
|
||||
builder.setIcon(R.mipmap.ic_launcher)
|
||||
builder.setView(textView)
|
||||
builder.setNeutralButton(
|
||||
R.string.main_help_close
|
||||
) { _: 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(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] = LibUIUtils.getGnssDisplayName(app, gnssType)
|
||||
if (filter.contains(gnssType)) {
|
||||
checks[i] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Arguments
|
||||
val args = Bundle()
|
||||
args.putStringArray(GnssFilterDialog.ITEMS, items)
|
||||
args.putBooleanArray(GnssFilterDialog.CHECKS, checks)
|
||||
val frag = GnssFilterDialog()
|
||||
frag.arguments = args
|
||||
frag.show(activity.supportFragmentManager, ".GnssFilterDialog")
|
||||
}
|
||||
|
||||
fun showSortByDialog(activity: FragmentActivity) {
|
||||
// TODO - convert all dialogs to MaterialAlertDialog (https://material.io/components/dialogs/android#using-dialogs)
|
||||
val builder = AlertDialog.Builder(
|
||||
activity
|
||||
)
|
||||
builder.setTitle(R.string.menu_option_sort_by)
|
||||
val currentSatOrder = PreferenceUtils.getSatSortOrderFromPreferences(app, prefs)
|
||||
builder.setSingleChoiceItems(
|
||||
R.array.sort_sats, currentSatOrder
|
||||
) { dialog: DialogInterface, index: Int ->
|
||||
LibUIUtils.setSortByClause(app, index, prefs)
|
||||
dialog.dismiss()
|
||||
}
|
||||
val dialog = builder.create()
|
||||
dialog.setOwnerActivity(activity)
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.android.gpstest.view;
|
||||
|
||||
import static com.android.gpstest.library.model.SatelliteStatus.NO_DATA;
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
@@ -7,42 +10,29 @@ import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RectF;
|
||||
import android.location.GnssMeasurementsEvent;
|
||||
import android.location.GnssStatus;
|
||||
import android.location.GpsSatellite;
|
||||
import android.location.GpsStatus;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.android.gpstest.Application;
|
||||
import com.android.gpstest.GpsTestListener;
|
||||
import com.android.gpstest.R;
|
||||
import com.android.gpstest.model.GnssType;
|
||||
import com.android.gpstest.util.MathUtils;
|
||||
import com.android.gpstest.util.SatelliteUtils;
|
||||
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.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* View that shows satellite positions on a circle representing the sky
|
||||
*/
|
||||
|
||||
public class GpsSkyView extends View implements GpsTestListener {
|
||||
public class GpsSkyView extends View {
|
||||
|
||||
public static final float MIN_VALUE_CN0 = 10.0f;
|
||||
public static final float MAX_VALUE_CN0 = 45.0f;
|
||||
public static final float MIN_VALUE_SNR = 0.0f;
|
||||
public static final float MAX_VALUE_SNR = 30.0f;
|
||||
|
||||
// View dimensions, to draw the compass with the correct width and height
|
||||
private static int mHeight;
|
||||
@@ -53,10 +43,6 @@ public class GpsSkyView extends View implements GpsTestListener {
|
||||
|
||||
private static int SAT_RADIUS;
|
||||
|
||||
private float[] mSnrThresholds;
|
||||
|
||||
private int[] mSnrColors;
|
||||
|
||||
private float[] mCn0Thresholds;
|
||||
|
||||
private int[] mCn0Colors;
|
||||
@@ -73,26 +59,11 @@ public class GpsSkyView extends View implements GpsTestListener {
|
||||
|
||||
private boolean mStarted;
|
||||
|
||||
private float[] mSnrCn0s; // Holds either SNR or C/N0 - see #65
|
||||
private float[] mElevs;
|
||||
private float[] mAzims;
|
||||
private float mCn0UsedAvg = 0.0f;
|
||||
|
||||
private float mSnrCn0UsedAvg = 0.0f;
|
||||
private float mCn0InViewAvg = 0.0f;
|
||||
|
||||
private float mSnrCn0InViewAvg = 0.0f;
|
||||
|
||||
private boolean[] mHasEphemeris;
|
||||
private boolean[] mHasAlmanac;
|
||||
private boolean[] mUsedInFix;
|
||||
|
||||
private int[] mPrns;
|
||||
private int[] mConstellationType;
|
||||
|
||||
private int mSvCount;
|
||||
|
||||
private boolean mUseLegacyGnssApi = false;
|
||||
|
||||
private boolean mIsSnrBad = false;
|
||||
private List<SatelliteStatus> statuses = emptyList();
|
||||
|
||||
public GpsSkyView(Context context) {
|
||||
super(context);
|
||||
@@ -107,12 +78,12 @@ public class GpsSkyView extends View implements GpsTestListener {
|
||||
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;
|
||||
int satStrokeColorUsed;
|
||||
if (Application.getPrefs().getBoolean(mContext.getString(R.string.pref_key_dark_theme), false)) {
|
||||
if (Application.Companion.getPrefs().getBoolean(mContext.getString(R.string.pref_key_dark_theme), false)) {
|
||||
// Dark theme
|
||||
textColor = getResources().getColor(android.R.color.secondary_text_dark);
|
||||
backgroundColor = ContextCompat.getColor(context, R.color.navdrawer_background_dark);
|
||||
@@ -162,12 +133,6 @@ public class GpsSkyView extends View implements GpsTestListener {
|
||||
mSatelliteUsedStrokePaint.setStrokeWidth(8.0f);
|
||||
mSatelliteUsedStrokePaint.setAntiAlias(true);
|
||||
|
||||
mSnrThresholds = new float[]{MIN_VALUE_SNR, 10.0f, 20.0f, MAX_VALUE_SNR};
|
||||
mSnrColors = new int[]{ContextCompat.getColor(mContext, R.color.gray),
|
||||
ContextCompat.getColor(mContext, R.color.red),
|
||||
ContextCompat.getColor(mContext, R.color.yellow),
|
||||
ContextCompat.getColor(mContext, R.color.green)};
|
||||
|
||||
mCn0Thresholds = new float[]{MIN_VALUE_CN0, 21.67f, 33.3f, MAX_VALUE_CN0};
|
||||
mCn0Colors = new int[]{ContextCompat.getColor(mContext, R.color.gray),
|
||||
ContextCompat.getColor(mContext, R.color.red),
|
||||
@@ -190,7 +155,7 @@ public class GpsSkyView extends View implements GpsTestListener {
|
||||
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();
|
||||
@@ -203,13 +168,10 @@ public class GpsSkyView extends View implements GpsTestListener {
|
||||
|
||||
// Get the proper height and width of view before drawing
|
||||
getViewTreeObserver().addOnPreDrawListener(
|
||||
new ViewTreeObserver.OnPreDrawListener() {
|
||||
@Override
|
||||
public boolean onPreDraw() {
|
||||
mHeight = getHeight();
|
||||
mWidth = getWidth();
|
||||
return true;
|
||||
}
|
||||
() -> {
|
||||
mHeight = getHeight();
|
||||
mWidth = getWidth();
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -221,148 +183,41 @@ public class GpsSkyView extends View implements GpsTestListener {
|
||||
|
||||
public void setStopped() {
|
||||
mStarted = false;
|
||||
mSvCount = 0;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public synchronized void setGnssStatus(GnssStatus status) {
|
||||
mUseLegacyGnssApi = false;
|
||||
mIsSnrBad = false;
|
||||
if (mPrns == null) {
|
||||
/**
|
||||
* We need to allocate arrays big enough so we don't overflow them. Per
|
||||
* https://developer.android.com/reference/android/location/GnssStatus.html#getSvid(int)
|
||||
* 255 should be enough to contain all known satellites world-wide.
|
||||
*/
|
||||
final int MAX_LENGTH = 255;
|
||||
mPrns = new int[MAX_LENGTH];
|
||||
mSnrCn0s = new float[MAX_LENGTH];
|
||||
mElevs = new float[MAX_LENGTH];
|
||||
mAzims = new float[MAX_LENGTH];
|
||||
mConstellationType = new int[MAX_LENGTH];
|
||||
mHasEphemeris = new boolean[MAX_LENGTH];
|
||||
mHasAlmanac = new boolean[MAX_LENGTH];
|
||||
mUsedInFix = new boolean[MAX_LENGTH];
|
||||
}
|
||||
public synchronized void setStatus(List<SatelliteStatus> statuses) {
|
||||
this.statuses = statuses;
|
||||
|
||||
int length = status.getSatelliteCount();
|
||||
mSvCount = 0;
|
||||
int svInViewCount = 0;
|
||||
int svUsedCount = 0;
|
||||
float cn0InViewSum = 0.0f;
|
||||
float cn0UsedSum = 0.0f;
|
||||
mSnrCn0InViewAvg = 0.0f;
|
||||
mSnrCn0UsedAvg = 0.0f;
|
||||
while (mSvCount < length) {
|
||||
mSnrCn0s[mSvCount] = status.getCn0DbHz(mSvCount); // Store C/N0 values (see #65)
|
||||
mElevs[mSvCount] = status.getElevationDegrees(mSvCount);
|
||||
mAzims[mSvCount] = status.getAzimuthDegrees(mSvCount);
|
||||
mPrns[mSvCount] = status.getSvid(mSvCount);
|
||||
mConstellationType[mSvCount] = status.getConstellationType(mSvCount);
|
||||
mHasEphemeris[mSvCount] = status.hasEphemerisData(mSvCount);
|
||||
mHasAlmanac[mSvCount] = status.hasAlmanacData(mSvCount);
|
||||
mUsedInFix[mSvCount] = status.usedInFix(mSvCount);
|
||||
mCn0InViewAvg = 0.0f;
|
||||
mCn0UsedAvg = 0.0f;
|
||||
for (SatelliteStatus s : statuses) {
|
||||
// If satellite is in view, add signal to calculate avg
|
||||
if (status.getCn0DbHz(mSvCount) != 0.0f) {
|
||||
if (s.getCn0DbHz() != 0.0f) {
|
||||
svInViewCount++;
|
||||
cn0InViewSum = cn0InViewSum + status.getCn0DbHz(mSvCount);
|
||||
cn0InViewSum = cn0InViewSum + s.getCn0DbHz();
|
||||
}
|
||||
if (status.usedInFix(mSvCount)) {
|
||||
if (s.getUsedInFix()) {
|
||||
svUsedCount++;
|
||||
cn0UsedSum = cn0UsedSum + status.getCn0DbHz(mSvCount);
|
||||
cn0UsedSum = cn0UsedSum + s.getCn0DbHz();
|
||||
}
|
||||
mSvCount++;
|
||||
}
|
||||
|
||||
if (svInViewCount > 0) {
|
||||
mSnrCn0InViewAvg = cn0InViewSum / svInViewCount;
|
||||
mCn0InViewAvg = cn0InViewSum / svInViewCount;
|
||||
}
|
||||
if (svUsedCount > 0) {
|
||||
mSnrCn0UsedAvg = cn0UsedSum / svUsedCount;
|
||||
mCn0UsedAvg = cn0UsedSum / svUsedCount;
|
||||
}
|
||||
|
||||
mStarted = true;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public void setGnssMeasurementEvent(GnssMeasurementsEvent event) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void setSats(GpsStatus status) {
|
||||
mUseLegacyGnssApi = true;
|
||||
Iterator<GpsSatellite> satellites = status.getSatellites().iterator();
|
||||
|
||||
if (mSnrCn0s == null) {
|
||||
int length = status.getMaxSatellites();
|
||||
mSnrCn0s = new float[length];
|
||||
mElevs = new float[length];
|
||||
mAzims = new float[length];
|
||||
mPrns = new int[length];
|
||||
mHasEphemeris = new boolean[length];
|
||||
mHasAlmanac = new boolean[length];
|
||||
mUsedInFix = new boolean[length];
|
||||
// Constellation type isn't used, but instantiate it to avoid NPE in legacy devices
|
||||
mConstellationType = new int[length];
|
||||
}
|
||||
|
||||
mSvCount = 0;
|
||||
int svInViewCount = 0;
|
||||
int svUsedCount = 0;
|
||||
float snrInViewSum = 0.0f;
|
||||
float snrUsedSum = 0.0f;
|
||||
mSnrCn0InViewAvg = 0.0f;
|
||||
mSnrCn0UsedAvg = 0.0f;
|
||||
while (satellites.hasNext()) {
|
||||
GpsSatellite satellite = satellites.next();
|
||||
mSnrCn0s[mSvCount] = satellite.getSnr(); // Store SNR values (see #65)
|
||||
mElevs[mSvCount] = satellite.getElevation();
|
||||
mAzims[mSvCount] = satellite.getAzimuth();
|
||||
mPrns[mSvCount] = satellite.getPrn();
|
||||
mHasEphemeris[mSvCount] = satellite.hasEphemeris();
|
||||
mHasAlmanac[mSvCount] = satellite.hasAlmanac();
|
||||
mUsedInFix[mSvCount] = satellite.usedInFix();
|
||||
// If satellite is in view, add signal to calculate avg
|
||||
if (satellite.getSnr() != 0.0f) {
|
||||
svInViewCount++;
|
||||
snrInViewSum = snrInViewSum + satellite.getSnr();
|
||||
}
|
||||
if (satellite.usedInFix()) {
|
||||
svUsedCount++;
|
||||
snrUsedSum = snrUsedSum + satellite.getSnr();
|
||||
}
|
||||
mSvCount++;
|
||||
}
|
||||
|
||||
if (svInViewCount > 0) {
|
||||
mSnrCn0InViewAvg = snrInViewSum / svInViewCount;
|
||||
}
|
||||
if (svUsedCount > 0) {
|
||||
mSnrCn0UsedAvg = snrUsedSum / svUsedCount;
|
||||
}
|
||||
|
||||
checkBadSnr();
|
||||
|
||||
mStarted = true;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the SNR values are bad (see #153)
|
||||
*/
|
||||
private void checkBadSnr() {
|
||||
if (mUseLegacyGnssApi) {
|
||||
// If either of the avg SNR values are greater than the max SNR value, mark the data as suspect
|
||||
if ((MathUtils.isValidFloat(mSnrCn0InViewAvg) && mSnrCn0InViewAvg > GpsSkyView.MAX_VALUE_SNR) ||
|
||||
(MathUtils.isValidFloat(mSnrCn0UsedAvg) && mSnrCn0UsedAvg > GpsSkyView.MAX_VALUE_SNR)) {
|
||||
mIsSnrBad = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void drawLine(Canvas c, float x1, float y1, float x2, float y2) {
|
||||
// rotate the line based on orientation
|
||||
double angle = Math.toRadians(-mOrientation);
|
||||
@@ -432,8 +287,8 @@ public class GpsSkyView extends View implements GpsTestListener {
|
||||
c.drawPath(path, mNorthFillPaint);
|
||||
}
|
||||
|
||||
private void drawSatellite(Canvas c, int s, float elev, float azim, float snrCn0, int prn,
|
||||
int constellationType, boolean usedInFix) {
|
||||
private void drawSatellite(Canvas c, int s, float elev, float azim, float cn0, int prn,
|
||||
GnssType gnssType, boolean usedInFix) {
|
||||
double radius, angle;
|
||||
float x, y;
|
||||
// Place PRN text slightly below drawn satellite
|
||||
@@ -441,12 +296,12 @@ public class GpsSkyView extends View implements GpsTestListener {
|
||||
final double PRN_Y_SCALE = 3.8;
|
||||
|
||||
Paint fillPaint;
|
||||
if (snrCn0 == 0.0f) {
|
||||
if (cn0 == 0.0f) {
|
||||
// Satellite can't be seen
|
||||
fillPaint = mNotInViewPaint;
|
||||
} else {
|
||||
// Calculate fill color based on signal strength
|
||||
fillPaint = getSatellitePaint(mSatelliteFillPaint, snrCn0);
|
||||
fillPaint = getSatellitePaint(mSatelliteFillPaint, cn0);
|
||||
}
|
||||
|
||||
Paint strokePaint;
|
||||
@@ -463,14 +318,8 @@ public class GpsSkyView extends View implements GpsTestListener {
|
||||
x = (float) ((s / 2) + (radius * Math.sin(angle)));
|
||||
y = (float) ((s / 2) - (radius * Math.cos(angle)));
|
||||
|
||||
// Change shape based on satellite operator
|
||||
GnssType operator;
|
||||
if (SatelliteUtils.isGnssStatusListenerSupported() && !mUseLegacyGnssApi) {
|
||||
operator = SatelliteUtils.getGnssConstellationType(constellationType);
|
||||
} else {
|
||||
operator = SatelliteUtils.getGnssType(prn);
|
||||
}
|
||||
switch (operator) {
|
||||
// Change shape based on satellite gnssType
|
||||
switch (gnssType) {
|
||||
case NAVSTAR:
|
||||
c.drawCircle(x, y, SAT_RADIUS, fillPaint);
|
||||
c.drawCircle(x, y, SAT_RADIUS, strokePaint);
|
||||
@@ -584,49 +433,39 @@ public class GpsSkyView extends View implements GpsTestListener {
|
||||
c.drawOval(rect, strokePaint);
|
||||
}
|
||||
|
||||
private Paint getSatellitePaint(Paint base, float snrCn0) {
|
||||
Paint newPaint;
|
||||
newPaint = new Paint(base);
|
||||
newPaint.setColor(getSatelliteColor(snrCn0));
|
||||
private Paint getSatellitePaint(Paint base, float cn0) {
|
||||
Paint newPaint = new Paint(base);
|
||||
newPaint.setColor(getSatelliteColor(cn0));
|
||||
return newPaint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the paint color for a satellite based on provided SNR or C/N0 and the thresholds defined in this class
|
||||
* Gets the paint color for a satellite based on provided C/N0 and the thresholds defined in this class
|
||||
*
|
||||
* @param snrCn0 the SNR to use (if using legacy GpsStatus) or the C/N0 to use (if using is
|
||||
* GnssStatus) to generate the satellite color based on signal quality
|
||||
* @return the paint color for a satellite based on provided SNR or C/N0
|
||||
* @param cn0 the C/N0 to use to generate the satellite color based on signal quality
|
||||
* @return the paint color for a satellite based on provided C/N0
|
||||
*/
|
||||
public synchronized int getSatelliteColor(float snrCn0) {
|
||||
public synchronized int getSatelliteColor(float cn0) {
|
||||
int numSteps;
|
||||
final float[] thresholds;
|
||||
final int[] colors;
|
||||
|
||||
if (!mUseLegacyGnssApi || mIsSnrBad) {
|
||||
// Use C/N0 ranges/colors for both C/N0 and SNR on Android 7.0 and higher (see #76)
|
||||
numSteps = mCn0Thresholds.length;
|
||||
thresholds = mCn0Thresholds;
|
||||
colors = mCn0Colors;
|
||||
} else {
|
||||
// Use legacy SNR ranges/colors for Android versions less than Android 7.0 or if user selects legacy API (see #76)
|
||||
numSteps = mSnrThresholds.length;
|
||||
thresholds = mSnrThresholds;
|
||||
colors = mSnrColors;
|
||||
}
|
||||
numSteps = mCn0Thresholds.length;
|
||||
thresholds = mCn0Thresholds;
|
||||
colors = mCn0Colors;
|
||||
|
||||
if (snrCn0 <= thresholds[0]) {
|
||||
if (cn0 <= thresholds[0]) {
|
||||
return colors[0];
|
||||
}
|
||||
|
||||
if (snrCn0 >= thresholds[numSteps - 1]) {
|
||||
if (cn0 >= thresholds[numSteps - 1]) {
|
||||
return colors[numSteps - 1];
|
||||
}
|
||||
|
||||
for (int i = 0; i < numSteps - 1; i++) {
|
||||
float threshold = thresholds[i];
|
||||
float nextThreshold = thresholds[i + 1];
|
||||
if (snrCn0 >= threshold && snrCn0 <= nextThreshold) {
|
||||
if (cn0 >= threshold && cn0 <= nextThreshold) {
|
||||
int c1, r1, g1, b1, c2, r2, g2, b2, c3, r3, g3, b3;
|
||||
float f;
|
||||
|
||||
@@ -640,7 +479,7 @@ public class GpsSkyView extends View implements GpsTestListener {
|
||||
g2 = Color.green(c2);
|
||||
b2 = Color.blue(c2);
|
||||
|
||||
f = (snrCn0 - threshold) / (nextThreshold - threshold);
|
||||
f = (cn0 - threshold) / (nextThreshold - threshold);
|
||||
|
||||
r3 = (int) (r2 * f + r1 * (1.0f - f));
|
||||
g3 = (int) (g2 * f + g1 * (1.0f - f));
|
||||
@@ -657,20 +496,21 @@ public class GpsSkyView extends View implements GpsTestListener {
|
||||
protected void onDraw(Canvas canvas) {
|
||||
int minScreenDimen;
|
||||
|
||||
minScreenDimen = (mWidth < mHeight) ? mWidth : mHeight;
|
||||
minScreenDimen = Math.min(mWidth, mHeight);
|
||||
|
||||
drawHorizon(canvas, minScreenDimen);
|
||||
|
||||
drawNorthIndicator(canvas, minScreenDimen);
|
||||
|
||||
if (mElevs != null) {
|
||||
int numSats = mSvCount;
|
||||
|
||||
for (int i = 0; i < numSats; i++) {
|
||||
if (mElevs[i] != 0.0f || mAzims[i] != 0.0f) {
|
||||
drawSatellite(canvas, minScreenDimen, mElevs[i], mAzims[i], mSnrCn0s[i],
|
||||
mPrns[i], mConstellationType[i], mUsedInFix[i]);
|
||||
}
|
||||
for (SatelliteStatus s : statuses) {
|
||||
if (s.getElevationDegrees() != NO_DATA && s.getAzimuthDegrees() != NO_DATA) {
|
||||
drawSatellite(canvas, minScreenDimen,
|
||||
s.getElevationDegrees(),
|
||||
s.getAzimuthDegrees(),
|
||||
s.getCn0DbHz(),
|
||||
s.getSvid(),
|
||||
s.getGnssType(),
|
||||
s.getUsedInFix());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -683,108 +523,24 @@ public class GpsSkyView extends View implements GpsTestListener {
|
||||
setMeasuredDimension(specSize, specSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOrientationChanged(double orientation, double tilt) {
|
||||
mOrientation = orientation;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void gpsStart() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void gpsStop() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssFirstFix(int ttffMillis) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssFixAcquired() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssFixLost() {
|
||||
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@Override
|
||||
public void onSatelliteStatusChanged(GnssStatus status) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssStarted() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssStopped() {
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@Override
|
||||
public void onGnssMeasurementsReceived(GnssMeasurementsEvent event) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNmeaMessage(String message, long timestamp) {
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public void onGpsStatusChanged(int event, GpsStatus status) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationChanged(Location location) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(String provider, int status, Bundle extras) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderEnabled(String provider) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderDisabled(String provider) {
|
||||
/**
|
||||
* Returns the average signal strength (C/N0) for satellites that are in view of the device (i.e., value is not 0), or 0 if the average can't be calculated
|
||||
* @return the average signal strength (C/N0) for satellites that are in view of the device (i.e., value is not 0), or 0 if the average can't be calculated
|
||||
*/
|
||||
public synchronized float getCn0InViewAvg() {
|
||||
return mCn0InViewAvg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the average signal strength (C/N0 if isUsingLegacyGpsApi is false, SNR if isUsingLegacyGpsApi is true) for satellites that are in view of the device (i.e., value is not 0), or 0 if the average can't be calculated
|
||||
* @return the average signal strength (C/N0 if isUsingLegacyGpsApi is false, SNR if isUsingLegacyGpsApi is true) for satellites that are in view of the device (i.e., value is not 0), or 0 if the average can't be calculated
|
||||
* Returns the average signal strength (C/N0) for satellites that are being used to calculate a location fix, or 0 if the average can't be calculated
|
||||
* @return the average signal strength (C/N0) for satellites that are being used to calculate a location fix, or 0 if the average can't be calculated
|
||||
*/
|
||||
public synchronized float getSnrCn0InViewAvg() {
|
||||
return mSnrCn0InViewAvg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the average signal strength (C/N0 if isUsingLegacyGpsApi is false, SNR if isUsingLegacyGpsApi is true) for satellites that are being used to calculate a location fix, or 0 if the average can't be calculated
|
||||
* @return the average signal strength (C/N0 if isUsingLegacyGpsApi is false, SNR if isUsingLegacyGpsApi is true) for satellites that are being used to calculate a location fix, or 0 if the average can't be calculated
|
||||
*/
|
||||
public synchronized float getSnrCn0UsedAvg() {
|
||||
return mSnrCn0UsedAvg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the app is monitoring the legacy GpsStatus.Listener, or false if the app is monitoring the GnssStatus.Callback
|
||||
* @return true if the app is monitoring the legacy GpsStatus.Listener, or false if the app is monitoring the GnssStatus.Callback
|
||||
*/
|
||||
public synchronized boolean isUsingLegacyGpsApi() {
|
||||
return mUseLegacyGnssApi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if bad SNR data has been detected (avgs exceeded max SNR threshold), or false if no SNR is observed (i.e., C/N0 data is observed) or SNR data seems ok
|
||||
* @return true if bad SNR data has been detected (avgs exceeded max SNR threshold), or false if no SNR is observed (i.e., C/N0 data is observed) or SNR data seems ok
|
||||
*/
|
||||
public synchronized boolean isSnrBad() {
|
||||
return mIsSnrBad;
|
||||
public synchronized float getCn0UsedAvg() {
|
||||
return mCn0UsedAvg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,10 +26,10 @@ import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
import com.android.gpstest.R;
|
||||
|
||||
import androidx.core.view.ViewCompat;
|
||||
|
||||
import com.android.gpstest.R;
|
||||
|
||||
/**
|
||||
* A layout that draws something in the insets passed to {@link #fitSystemWindows(android.graphics.Rect)},
|
||||
* i.e. the area above UI chrome
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<group android:scaleX="3.375"
|
||||
android:scaleY="3.375"
|
||||
android:translateX="13.5"
|
||||
android:translateY="13.5">
|
||||
<path
|
||||
android:pathData="M8.0176,5.498L5.0176,8.5078L7.1934,10.6836L5.0176,12.5078L8.0742,15.5645L5.0176,18.5078L10.5801,24.0723L23.9434,24.0723L23.9434,17.4238L12.0176,5.498L10.0781,7.5586L8.0176,5.498z"
|
||||
android:strokeAlpha="0.03490196"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="0"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="0.1"
|
||||
android:strokeLineCap="butt"/>
|
||||
<path
|
||||
android:pathData="m5.0174,5.4985h3c0,1.66 -1.34,3.01 -3,3.01z"
|
||||
android:fillColor="#3f51b5"/>
|
||||
<path
|
||||
android:pathData="m5.0174,12.5085v-2c2.76,0 5,-2.25 5,-5.01h2c0,3.87 -3.13,7.01 -7,7.01z"
|
||||
android:fillColor="#3f51b5"/>
|
||||
<path
|
||||
android:pathData="m5.0174,18.5085 l3.5,-4.5 2.5,3.01 3.5,-4.51 4.5,6z"
|
||||
android:fillColor="#3f51b5"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -9,7 +9,7 @@
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
tools:context="com.android.gpstest.HelpActivity">
|
||||
tools:context="com.android.gpstest.ui.HelpActivity">
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context="com.android.gpstest.GpsTestActivity">
|
||||
tools:context="com.android.gpstest.ui.MainActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_height="wrap_content"
|
||||
@@ -58,7 +58,7 @@
|
||||
android:layout_width="@dimen/navigation_drawer_width"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:name="com.android.gpstest.NavigationDrawerFragment"
|
||||
android:name="com.android.gpstest.ui.NavigationDrawerFragment"
|
||||
tools:layout="@layout/navigation_drawer"/>
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
</LinearLayout>
|
||||
@@ -21,17 +21,17 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/large_screen_layout">
|
||||
<fragment class="com.android.gpstest.GpsStatusFragment"
|
||||
<fragment class="com.android.gpstest.ui.status.StatusFragment"
|
||||
android:id="@+id/gps_status_fragment"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"/>
|
||||
<fragment class="com.android.gpstest.GpsMapFragment"
|
||||
<fragment class="com.android.gpstest.ui.MapFragment"
|
||||
android:id="@+id/gps_map_fragment"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"/>
|
||||
<fragment class="com.android.gpstest.GpsSkyFragment"
|
||||
<fragment class="com.android.gpstest.ui.sky.SkyFragment"
|
||||
android:id="@+id/gps_sky_fragment"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="com.android.gpstest.GpsTestActivity">
|
||||
tools:context="com.android.gpstest.ui.MainActivity">
|
||||
|
||||
<include layout="@layout/content_main"/>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:minWidth="@dimen/cn0_value_min_width"
|
||||
android:text="38.2"
|
||||
style="@style/cn0_avg_value_in_view"
|
||||
android:layout_span="2"
|
||||
android:layout_alignParentRight="true" />
|
||||
android:id="@+id/cn0_text_in_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:minWidth="@dimen/cn0_value_min_width"
|
||||
android:text="38.2"
|
||||
style="@style/cn0_avg_value_in_view"
|
||||
android:layout_span="2"
|
||||
android:layout_alignParentRight="true" />
|
||||
@@ -1,10 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:minWidth="@dimen/cn0_value_min_width"
|
||||
android:text="25.0"
|
||||
style="@style/cn0_avg_value_used"
|
||||
android:layout_span="2"
|
||||
android:layout_alignParentRight="true" />
|
||||
android:id="@+id/cn0_text_used"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:minWidth="@dimen/cn0_value_min_width"
|
||||
android:text="25.0"
|
||||
style="@style/cn0_avg_value_used"
|
||||
android:layout_span="2"
|
||||
android:layout_alignParentRight="true" />
|
||||
@@ -35,7 +35,7 @@
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
tools:showIn="@layout/app_bar_main"
|
||||
tools:context="com.android.gpstest.GpsTestActivity"/>
|
||||
tools:context="com.android.gpstest.ui.MainActivity"/>
|
||||
|
||||
<!-- Top card to enter ground truth information - CardView first, then MotionLayout
|
||||
(MotionLayout doesn't resize dynamically well during animations, so instead of
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<RelativeLayout
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false">
|
||||
@@ -40,11 +40,21 @@
|
||||
tools:visibility="visible"
|
||||
android:contentDescription="@string/lock"/>
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/filter_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="5dp"/>
|
||||
|
||||
<com.android.gpstest.view.GpsSkyView
|
||||
android:id="@+id/sky_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp"/>
|
||||
android:layout_margin="5dp"
|
||||
android:layout_below="@id/filter_view"/>
|
||||
|
||||
<include layout="@layout/gps_sky_cn0_indicator_card"
|
||||
android:id="@+id/sky_cn0_indicator_card"
|
||||
@@ -57,8 +67,8 @@
|
||||
android:layout_marginEnd="@dimen/sky_margin"
|
||||
android:layout_below="@id/sky_view"/>
|
||||
|
||||
<include layout="@layout/gps_sky_shape_legend_card"
|
||||
android:id="@+id/sky_legend_shape"
|
||||
<include layout="@layout/gps_sky_legend_card"
|
||||
android:id="@+id/sky_legend_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/sky_cn0_indicator_card"
|
||||
|
||||
@@ -37,15 +37,15 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false">
|
||||
<include layout="@layout/gps_sky_signal_title"
|
||||
android:id="@+id/gps_sky_signal_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toLeftOf="@+id/sky_legend_cn0"
|
||||
android:layout_toStartOf="@+id/sky_legend_cn0"
|
||||
android:layout_toStartOf="@+id/gps_sky_signal_meter"
|
||||
android:layout_marginTop="16dp"
|
||||
android:paddingTop="5dp"
|
||||
android:layout_centerHorizontal="true" />
|
||||
<include layout="@layout/gps_sky_signal_meter"
|
||||
android:id="@+id/sky_legend_cn0"
|
||||
android:id="@+id/gps_sky_signal_meter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
app:srcCompat="@drawable/square"
|
||||
android:contentDescription="@string/square"/>/>
|
||||
android:contentDescription="@string/square"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/sky_legend_shape_line2a"
|
||||
@@ -684,6 +684,55 @@
|
||||
android:layout_margin="5dp"
|
||||
android:text="@string/sky_legend_shape_saccsa" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/sky_legend_diamond8"
|
||||
android:layout_width="@dimen/sky_legend_shape_size"
|
||||
android:layout_height="@dimen/sky_legend_shape_size"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
app:srcCompat="@drawable/diamond"
|
||||
android:contentDescription="@string/diamond"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/sky_legend_shape_line17a"
|
||||
android:layout_width="@dimen/sky_legend_shape_small_line"
|
||||
android:layout_height="1dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:background="@color/body_text_2_light" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/legend_flag_southpan"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:background="@color/body_text_2_light"
|
||||
android:padding="1dp"
|
||||
app:srcCompat="@drawable/ic_flag_southpan"
|
||||
android:contentDescription="@string/southpan_flag"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/sky_legend_shape_line17b"
|
||||
android:layout_width="@dimen/sky_legend_shape_small_line"
|
||||
android:layout_height="1dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="5dp"
|
||||
android:background="@color/body_text_2_light" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sky_southpan_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp"
|
||||
android:text="@string/sky_legend_shape_southpan" />
|
||||
</TableRow>
|
||||
|
||||
</TableLayout>
|
||||
|
||||
<TextView
|
||||
@@ -793,4 +842,4 @@
|
||||
</TableRow>
|
||||
</TableLayout>
|
||||
</RelativeLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</androidx.cardview.widget.CardView>
|
||||
@@ -54,9 +54,9 @@
|
||||
android:layout_marginLeft="@dimen/cn0_indicator_min_left_margin"
|
||||
android:layout_marginStart="@dimen/cn0_indicator_min_left_margin"
|
||||
android:layout_marginBottom="-3dp"
|
||||
android:tint="@color/red"
|
||||
app:srcCompat="@drawable/ic_cn0_indicator"
|
||||
android:visibility="invisible"/>
|
||||
android:visibility="invisible"
|
||||
app:tint="@color/red" />
|
||||
|
||||
<include
|
||||
android:id="@+id/cn0_text_in_view"
|
||||
@@ -95,6 +95,7 @@
|
||||
|
||||
<include
|
||||
layout="@layout/signal_meter_ticks_and_text"
|
||||
android:id="@+id/signal_meter_ticks_and_text"
|
||||
android:layout_width="@dimen/cn0_meter_width"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/sky_legend_cn0"
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/gps_avg_cn0_label"
|
||||
android:text="@string/avg_cn0_label"
|
||||
android:textAlignment="center" />
|
||||
<TextView
|
||||
android:id="@+id/sky_legend_cn0_title"
|
||||
|
||||
@@ -1,330 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
/*
|
||||
** Copyright 2008, The Android Open Source Project
|
||||
**
|
||||
** 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.
|
||||
*/
|
||||
-->
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:card_view="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_marginLeft="@dimen/status_margin"
|
||||
android:layout_marginStart="@dimen/status_margin"
|
||||
android:layout_marginRight="@dimen/status_margin"
|
||||
android:layout_marginEnd="@dimen/status_margin"
|
||||
android:layout_marginTop="@dimen/status_margin"
|
||||
android:layout_marginBottom="@dimen/status_margin">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
xmlns:card_view="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/status_location_card"
|
||||
android:layout_gravity="center"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
card_view:cardUseCompatPadding="true"
|
||||
card_view:cardCornerRadius="4dp"
|
||||
android:foreground="?attr/selectableItemBackground">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/status_lock"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="6dp"
|
||||
card_view:srcCompat="@drawable/ic_baseline_lock_24"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:contentDescription="@string/lock"/>
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/status_location_scrollview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:foreground="?attr/selectableItemBackground">
|
||||
<TableLayout
|
||||
android:id="@+id/lat_long_table"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/status_margin"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<TableRow>
|
||||
<TextView
|
||||
android:id="@+id/latitude_label"
|
||||
style="@style/info_label"
|
||||
android:layout_gravity="right"
|
||||
android:layout_marginRight="@dimen/status_label_right_margin"
|
||||
android:text="@string/gps_latitude_label" />
|
||||
<TextView
|
||||
android:id="@+id/latitude"
|
||||
style="@style/info_value" />
|
||||
<TextView
|
||||
android:id="@+id/fix_time_label"
|
||||
style="@style/info_label"
|
||||
android:layout_gravity="right"
|
||||
android:layout_marginRight="@dimen/status_label_right_margin"
|
||||
android:text="@string/gps_fix_time_label" />
|
||||
<TextView
|
||||
android:id="@+id/fix_time"
|
||||
style="@style/info_value" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/fix_time_error"
|
||||
style="@style/info_value_error"
|
||||
android:visibility="gone" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TextView
|
||||
android:id="@+id/longitude_label"
|
||||
style="@style/info_label"
|
||||
android:layout_gravity="right"
|
||||
android:layout_marginRight="@dimen/status_label_right_margin"
|
||||
android:text="@string/gps_longitude_label" />
|
||||
<TextView
|
||||
android:id="@+id/longitude"
|
||||
style="@style/info_value" />
|
||||
<TextView
|
||||
android:id="@+id/gps_ttff_label"
|
||||
style="@style/info_label"
|
||||
android:layout_gravity="right"
|
||||
android:layout_marginRight="@dimen/status_label_right_margin"
|
||||
android:text="@string/gps_ttff_label" />
|
||||
<TextView
|
||||
android:id="@+id/ttff"
|
||||
style="@style/info_value" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TextView
|
||||
android:id="@+id/altitude_label"
|
||||
style="@style/info_label"
|
||||
android:layout_gravity="right"
|
||||
android:layout_marginRight="@dimen/status_label_right_margin"
|
||||
android:text="@string/gps_altitude_label" />
|
||||
<TextView
|
||||
android:id="@+id/altitude"
|
||||
style="@style/info_value" />
|
||||
<TextView
|
||||
android:id="@+id/hor_vert_accuracy_label"
|
||||
style="@style/info_label"
|
||||
android:layout_gravity="right"
|
||||
android:layout_marginRight="@dimen/status_label_right_margin"
|
||||
android:text="@string/gps_accuracy_label" />
|
||||
<TextView
|
||||
android:id="@+id/hor_vert_accuracy"
|
||||
style="@style/info_value" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TextView
|
||||
android:id="@+id/altitude_msl_label"
|
||||
style="@style/info_label"
|
||||
android:layout_gravity="right"
|
||||
android:layout_marginRight="@dimen/status_label_right_margin"
|
||||
android:text="@string/gps_altitude_msl_label" />
|
||||
<TextView
|
||||
android:id="@+id/altitude_msl"
|
||||
style="@style/info_value" />
|
||||
<TextView
|
||||
android:id="@+id/num_sats_label"
|
||||
style="@style/info_label"
|
||||
android:layout_gravity="right"
|
||||
android:layout_marginRight="@dimen/status_label_right_margin"
|
||||
android:text="@string/gps_num_sats_label" />
|
||||
<TextView
|
||||
android:id="@+id/num_sats"
|
||||
style="@style/info_value" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TextView
|
||||
android:id="@+id/speed_label"
|
||||
style="@style/info_label"
|
||||
android:layout_gravity="right"
|
||||
android:layout_marginRight="@dimen/status_label_right_margin"
|
||||
android:text="@string/gps_speed_label" />
|
||||
<TextView
|
||||
android:id="@+id/speed"
|
||||
style="@style/info_value" />
|
||||
<TextView
|
||||
android:id="@+id/bearing_label"
|
||||
style="@style/info_label"
|
||||
android:layout_gravity="right"
|
||||
android:layout_marginRight="@dimen/status_label_right_margin"
|
||||
android:text="@string/gps_bearing_label" />
|
||||
<TextView
|
||||
android:id="@+id/bearing"
|
||||
style="@style/info_value" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow
|
||||
android:id="@+id/speed_bearing_acc_row"
|
||||
android:visibility="gone">
|
||||
<TextView
|
||||
android:id="@+id/speed_acc_label"
|
||||
style="@style/info_label"
|
||||
android:layout_gravity="right"
|
||||
android:layout_marginRight="@dimen/status_label_right_margin"
|
||||
android:text="@string/gps_speed_acc_label" />
|
||||
<TextView
|
||||
android:id="@+id/speed_acc"
|
||||
style="@style/info_value" />
|
||||
<TextView
|
||||
android:id="@+id/bearing_acc_label"
|
||||
style="@style/info_label"
|
||||
android:layout_gravity="right"
|
||||
android:layout_marginRight="@dimen/status_label_right_margin"
|
||||
android:text="@string/gps_bearing_acc_label" />
|
||||
<TextView
|
||||
android:id="@+id/bearing_acc"
|
||||
style="@style/info_value" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TextView
|
||||
android:id="@+id/pdop_label"
|
||||
style="@style/info_label"
|
||||
android:layout_gravity="right"
|
||||
android:layout_marginRight="@dimen/status_label_right_margin"
|
||||
android:text="@string/pdop_label"
|
||||
android:visibility="gone" />
|
||||
<TextView
|
||||
android:id="@+id/pdop"
|
||||
style="@style/info_value"
|
||||
android:visibility="gone" />
|
||||
<TextView
|
||||
android:id="@+id/hvdop_label"
|
||||
style="@style/info_label"
|
||||
android:layout_gravity="right"
|
||||
android:layout_marginRight="@dimen/status_label_right_margin"
|
||||
android:text="@string/hvdop_label"
|
||||
android:visibility="gone" />
|
||||
<TextView
|
||||
android:id="@+id/hvdop"
|
||||
style="@style/info_value"
|
||||
android:visibility="gone" />
|
||||
</TableRow>
|
||||
|
||||
</TableLayout>
|
||||
</HorizontalScrollView>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/status_filter_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/filter_text"
|
||||
tools:text="Showing 5 of 10 satellites"
|
||||
android:textStyle="italic"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:gravity="center_vertical|right"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/filter_show_all"
|
||||
android:textStyle="italic"
|
||||
android:text="@string/filter_showall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginLeft="2dp"
|
||||
android:gravity="center_vertical"
|
||||
android:bufferType="spannable"
|
||||
android:linksClickable="true"
|
||||
android:textColorLink="@color/colorAccent"/>
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/gnss_status_card"
|
||||
android:layout_gravity="center"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
card_view:cardUseCompatPadding="true"
|
||||
card_view:cardCornerRadius="4dp"
|
||||
android:foreground="?attr/selectableItemBackground">
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/gnss_status_scrollview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/gnss_status_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="5dp" />
|
||||
</HorizontalScrollView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/gnss_not_available"
|
||||
style="@style/info_value"
|
||||
android:gravity="center"
|
||||
android:text="@string/gnss_not_available"
|
||||
android:layout_margin="10dp"
|
||||
android:visibility="gone" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/sbas_status_card"
|
||||
android:layout_gravity="center"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
card_view:cardUseCompatPadding="true"
|
||||
card_view:cardCornerRadius="4dp"
|
||||
android:foreground="?attr/selectableItemBackground">
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/sbas_status_scrollview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/sbas_status_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="5dp" />
|
||||
</HorizontalScrollView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sbas_not_available"
|
||||
style="@style/info_value"
|
||||
android:gravity="center"
|
||||
android:layout_margin="10dp"
|
||||
android:text="@string/sbas_not_available"
|
||||
android:visibility="gone" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
/*
|
||||
** Copyright 2013, Sean J. Barbeau
|
||||
** Copyright 2013-2021, 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.
|
||||
@@ -25,5 +25,5 @@
|
||||
android:id="@+id/gps_switch"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginRight="@dimen/activity_horizontal_margin"/>
|
||||
android:layout_marginEnd="@dimen/activity_horizontal_margin"/>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingRight="12dp"
|
||||
android:paddingEnd="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notification_permission_instructions"
|
||||
style="?android:attr/textAppearance"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="14dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:text="@string/notification_permission_required_dialog_text"
|
||||
android:textSize="16sp"
|
||||
android:autoLink="web"
|
||||
android:linksClickable="true" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -36,19 +36,6 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/upload_gps_status_api"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="@dimen/button_margin"
|
||||
android:textIsSelectable="true"
|
||||
android:text="@string/upload_gps_status_api"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/upload"
|
||||
style="@style/Widget.AppTheme.Button.IconButton"
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
** Copyright 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.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/activity_horizontal_margin"
|
||||
android:layout_marginRight="@dimen/activity_horizontal_margin"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sv_id"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="36dp"
|
||||
android:layout_marginRight="@dimen/column_padding"
|
||||
android:layout_marginEnd="@dimen/column_padding"
|
||||
android:textSize="@dimen/status_text_size"/>
|
||||
<TextView
|
||||
android:id="@+id/gnss_flag_header"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="@dimen/min_column_width"
|
||||
android:layout_marginRight="@dimen/column_padding"
|
||||
android:layout_marginEnd="@dimen/column_padding"
|
||||
android:visibility="gone"
|
||||
style="@style/info_label"/>
|
||||
<!-- We need the LinearLayout to draw a thin border around the flag image, but keep the minWidth for the column -->
|
||||
<LinearLayout
|
||||
android:id="@+id/gnss_flag_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="@dimen/min_column_width"
|
||||
android:layout_marginRight="@dimen/column_padding"
|
||||
android:layout_marginEnd="@dimen/column_padding"
|
||||
android:visibility="gone">
|
||||
<ImageView
|
||||
android:id="@+id/gnss_flag"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/body_text_2_light"
|
||||
android:padding="1dp"
|
||||
android:visibility="gone"/>
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:id="@+id/carrier_frequency"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="36dp"
|
||||
android:layout_marginRight="@dimen/column_padding"
|
||||
android:layout_marginEnd="@dimen/column_padding"
|
||||
android:textSize="@dimen/status_text_size"/>
|
||||
<TextView
|
||||
android:id="@+id/signal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="@dimen/min_column_width"
|
||||
android:layout_marginRight="@dimen/column_padding"
|
||||
android:layout_marginEnd="@dimen/column_padding"
|
||||
android:textSize="@dimen/status_text_size"/>
|
||||
<TextView
|
||||
android:id="@+id/status_flags"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="@dimen/min_column_width"
|
||||
android:layout_marginRight="@dimen/column_padding"
|
||||
android:layout_marginEnd="@dimen/column_padding"
|
||||
android:textSize="@dimen/status_text_size"/>
|
||||
<TextView
|
||||
android:id="@+id/elevation"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="@dimen/min_column_width"
|
||||
android:layout_marginRight="@dimen/column_padding"
|
||||
android:layout_marginEnd="@dimen/column_padding"
|
||||
android:textSize="@dimen/status_text_size"/>
|
||||
<TextView
|
||||
android:id="@+id/azimuth"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="@dimen/min_column_width"
|
||||
android:layout_marginRight="@dimen/column_padding"
|
||||
android:layout_marginEnd="@dimen/column_padding"
|
||||
android:textSize="@dimen/status_text_size"/>
|
||||
</LinearLayout>
|
||||
@@ -17,6 +17,13 @@
|
||||
<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"
|
||||
|
||||
@@ -23,12 +23,5 @@
|
||||
android:orderInCategory="1"
|
||||
app:showAsAction="ifRoom"
|
||||
android:contentDescription="@string/menu_option_sort_by_content_description" />
|
||||
<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" />
|
||||
</group>
|
||||
</menu>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -39,13 +39,143 @@
|
||||
android:title="@string/preferences_preferred_distance_units_title"
|
||||
android:entries="@array/preferred_distance_units_entries"
|
||||
android:entryValues="@array/preferred_distance_units_values"
|
||||
android:defaultValue="1"></ListPreference>
|
||||
android:defaultValue="1" />
|
||||
<ListPreference
|
||||
android:key="@string/pref_key_preferred_speed_units_v2"
|
||||
android:title="@string/preferences_preferred_speed_units_title"
|
||||
android:entries="@array/preferred_speed_units_entries"
|
||||
android:entryValues="@array/preferred_speed_units_values"
|
||||
android:defaultValue="1"></ListPreference>
|
||||
android:defaultValue="1" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/pref_gps_category_title"
|
||||
android:key="@string/pref_key_gps_category">
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_show_notification"
|
||||
android:title="@string/pref_gnss_show_notification_title"
|
||||
android:summary="@string/pref_gnss_show_notification_summary"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_gnss_background"
|
||||
android:title="@string/pref_gnss_background_title"
|
||||
android:summary="@string/pref_gnss_background_summary"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_force_full_gnss_measurements"
|
||||
android:title="@string/pref_force_full_gnss_measurements_title"
|
||||
android:summary="@string/pref_force_full_gnss_measurements_summary"
|
||||
android:defaultValue="true" />
|
||||
<EditTextPreference
|
||||
android:key="@string/pref_key_gps_min_time"
|
||||
android:title="@string/pref_gps_min_time_title"
|
||||
android:summary="@string/pref_gps_min_time_summary"
|
||||
android:dialogTitle="@string/pref_gps_min_time_dialog_title"
|
||||
android:defaultValue="@string/pref_gps_min_time_default_sec" />
|
||||
<EditTextPreference
|
||||
android:key="@string/pref_key_gps_min_distance"
|
||||
android:title="@string/pref_gps_min_distance_title"
|
||||
android:summary="@string/pref_gps_min_distance_summary"
|
||||
android:dialogTitle="@string/pref_gps_min_distance_dialog_title"
|
||||
android:defaultValue="@string/pref_gps_min_distance_default_meters" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_auto_start_gps"
|
||||
android:title="@string/pref_auto_start_gps_title"
|
||||
android:summary="@string/pref_auto_start_gps_summary"
|
||||
android:defaultValue="true" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/pref_output_category_title">
|
||||
<PreferenceScreen
|
||||
android:title="@string/pref_output_category_title"
|
||||
android:summary="@string/pref_output_category_summary">
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/pref_file_output_category_title"
|
||||
android:key="@string/pref_key_file_output_category">
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_file_nmea_output"
|
||||
android:title="@string/pref_nmea_output_title"
|
||||
android:summary="@string/pref_file_nmea_output_summary"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_file_measurement_output"
|
||||
android:title="@string/pref_measurement_output_title"
|
||||
android:summary="@string/pref_file_measurement_output_summary"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_file_gnss_status_output"
|
||||
android:title="@string/pref_file_gnss_status_output_title"
|
||||
android:summary="@string/pref_file_gnss_status_output_summary"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_file_orientation_output"
|
||||
android:title="@string/pref_file_orientation_output_title"
|
||||
android:summary="@string/pref_file_orientation_output_summary"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_file_location_output"
|
||||
android:title="@string/pref_file_location_output_title"
|
||||
android:summary="@string/pref_file_location_output_summary"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_file_navigation_message_output"
|
||||
android:title="@string/pref_navigation_message_output_title"
|
||||
android:summary="@string/pref_file_navigation_message_output_summary"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_file_antenna_output_csv"
|
||||
android:title="@string/pref_file_antenna_output_csv_title"
|
||||
android:summary="@string/pref_file_antenna_output_csv_summary"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_file_antenna_output_json"
|
||||
android:title="@string/pref_file_antenna_output_json_title"
|
||||
android:summary="@string/pref_file_antenna_output_json_summary"
|
||||
android:defaultValue="false" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/pref_inject_assist_data_when_logging_category_title"
|
||||
android:key="@string/pref_key_inject_assist_data_when_logging_category">
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_inject_time_when_logging"
|
||||
android:title="@string/pref_inject_time_when_logging_title"
|
||||
android:summary="@string/pref_inject_time_when_logging_summary"
|
||||
android:defaultValue="true" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_inject_psds_when_logging"
|
||||
android:title="@string/pref_inject_psds_when_logging_title"
|
||||
android:summary="@string/pref_inject_psds_when_logging_summary"
|
||||
android:defaultValue="true" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/pref_as_output_category_title"
|
||||
android:key="@string/pref_key_as_android_monitor_category">
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_as_nmea_output"
|
||||
android:title="@string/pref_nmea_output_title"
|
||||
android:summary="@string/pref_as_nmea_output_summary"
|
||||
android:defaultValue="true" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_as_nmea_timestamp_output"
|
||||
android:title="@string/pref_nmea_timestamp_output_title"
|
||||
android:summary="@string/pref_nmea_timestamp_output_summary"
|
||||
android:defaultValue="true" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_as_measurement_output"
|
||||
android:title="@string/pref_measurement_output_title"
|
||||
android:summary="@string/pref_as_measurement_output_summary"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_as_navigation_message_output"
|
||||
android:title="@string/pref_navigation_message_output_title"
|
||||
android:summary="@string/pref_as_navigation_message_output_summary"
|
||||
android:defaultValue="false" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
@@ -71,103 +201,4 @@
|
||||
android:dependency="@string/pref_key_rotate_map_with_compass"
|
||||
android:defaultValue="true" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/pref_gps_category_title"
|
||||
android:key="@string/pref_key_gps_category">
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_auto_start_gps"
|
||||
android:title="@string/pref_auto_start_gps_title"
|
||||
android:summary="@string/pref_auto_start_gps_summary"
|
||||
android:defaultValue="true" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_force_full_gnss_measurements"
|
||||
android:title="@string/pref_force_full_gnss_measurements_title"
|
||||
android:summary="@string/pref_force_full_gnss_measurements_summary"
|
||||
android:defaultValue="true" />
|
||||
<EditTextPreference
|
||||
android:key="@string/pref_key_gps_min_time"
|
||||
android:title="@string/pref_gps_min_time_title"
|
||||
android:summary="@string/pref_gps_min_time_summary"
|
||||
android:dialogTitle="@string/pref_gps_min_time_dialog_title"
|
||||
android:defaultValue="@string/pref_gps_min_time_default_sec" />
|
||||
<EditTextPreference
|
||||
android:key="@string/pref_key_gps_min_distance"
|
||||
android:title="@string/pref_gps_min_distance_title"
|
||||
android:summary="@string/pref_gps_min_distance_summary"
|
||||
android:dialogTitle="@string/pref_gps_min_distance_dialog_title"
|
||||
android:defaultValue="@string/pref_gps_min_distance_default_meters" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_use_gnss_apis"
|
||||
android:title="@string/pref_use_gnss_apis_title"
|
||||
android:summary="@string/pref_use_gnss_apis_summary"
|
||||
android:defaultValue="true" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/pref_output_category_title">
|
||||
<PreferenceScreen
|
||||
android:title="@string/pref_output_category_title"
|
||||
android:summary="@string/pref_output_category_summary">
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/pref_file_output_category_title"
|
||||
android:key="@string/pref_key_file_output_category">
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_file_nmea_output"
|
||||
android:title="@string/pref_nmea_output_title"
|
||||
android:summary="@string/pref_file_nmea_output_summary"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_file_navigation_message_output"
|
||||
android:title="@string/pref_navigation_message_output_title"
|
||||
android:summary="@string/pref_file_navigation_message_output_summary"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_file_measurement_output"
|
||||
android:title="@string/pref_measurement_output_title"
|
||||
android:summary="@string/pref_file_measurement_output_summary"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_file_location_output"
|
||||
android:title="@string/pref_file_location_output_title"
|
||||
android:summary="@string/pref_file_location_output_summary"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_file_antenna_output_csv"
|
||||
android:title="@string/pref_file_antenna_output_csv_title"
|
||||
android:summary="@string/pref_file_antenna_output_csv_summary"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_file_antenna_output_json"
|
||||
android:title="@string/pref_file_antenna_output_json_title"
|
||||
android:summary="@string/pref_file_antenna_output_json_summary"
|
||||
android:defaultValue="false" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/pref_as_output_category_title"
|
||||
android:key="@string/pref_key_as_android_monitor_category">
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_as_nmea_output"
|
||||
android:title="@string/pref_nmea_output_title"
|
||||
android:summary="@string/pref_as_nmea_output_summary"
|
||||
android:defaultValue="true" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_as_nmea_timestamp_output"
|
||||
android:title="@string/pref_nmea_timestamp_output_title"
|
||||
android:summary="@string/pref_nmea_timestamp_output_summary"
|
||||
android:defaultValue="true" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_as_navigation_message_output"
|
||||
android:title="@string/pref_navigation_message_output_title"
|
||||
android:summary="@string/pref_as_navigation_message_output_summary"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="@string/pref_key_as_measurement_output"
|
||||
android:title="@string/pref_measurement_output_title"
|
||||
android:summary="@string/pref_as_measurement_output_summary"
|
||||
android:defaultValue="false" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
|
||||
@@ -1,450 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2008-2013 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.
|
||||
*/
|
||||
|
||||
package com.android.gpstest;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Color;
|
||||
import android.location.GnssMeasurementsEvent;
|
||||
import android.location.GnssStatus;
|
||||
import android.location.GpsStatus;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.android.gpstest.map.MapViewModelController;
|
||||
import com.android.gpstest.map.OnMapClickListener;
|
||||
import com.android.gpstest.util.MapUtils;
|
||||
import com.android.gpstest.util.MathUtils;
|
||||
|
||||
import org.osmdroid.config.Configuration;
|
||||
import org.osmdroid.events.MapEventsReceiver;
|
||||
import org.osmdroid.tileprovider.tilesource.ITileSource;
|
||||
import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase;
|
||||
import org.osmdroid.util.GeoPoint;
|
||||
import org.osmdroid.util.MapTileIndex;
|
||||
import org.osmdroid.views.MapView;
|
||||
import org.osmdroid.views.overlay.MapEventsOverlay;
|
||||
import org.osmdroid.views.overlay.Marker;
|
||||
import org.osmdroid.views.overlay.Polygon;
|
||||
import org.osmdroid.views.overlay.Polyline;
|
||||
import org.osmdroid.views.overlay.gestures.RotationGestureOverlay;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static com.android.gpstest.map.MapConstants.ALLOW_GROUND_TRUTH_CHANGE;
|
||||
import static com.android.gpstest.map.MapConstants.CAMERA_INITIAL_ZOOM;
|
||||
import static com.android.gpstest.map.MapConstants.DRAW_LINE_THRESHOLD_METERS;
|
||||
import static com.android.gpstest.map.MapConstants.GROUND_TRUTH;
|
||||
import static com.android.gpstest.map.MapConstants.MODE;
|
||||
import static com.android.gpstest.map.MapConstants.MODE_ACCURACY;
|
||||
import static com.android.gpstest.map.MapConstants.MODE_MAP;
|
||||
|
||||
public class GpsMapFragment extends Fragment implements GpsTestListener, MapViewModelController.MapInterface {
|
||||
|
||||
private static final String MAP_TYPE_SATELLITE = "mapbox.satellite";
|
||||
|
||||
private static final String MAP_TYPE_STREETS = "barbeau/cju1g27421a0w1fmvsy13tjfv";
|
||||
|
||||
private MapView mMap;
|
||||
|
||||
RotationGestureOverlay mRotationGestureOverlay;
|
||||
|
||||
Marker mMyLocationMarker;
|
||||
|
||||
Marker mGroundTruthMarker;
|
||||
|
||||
Polygon mHorAccPolygon;
|
||||
|
||||
Polyline mErrorLine;
|
||||
|
||||
List<Polyline> mPathLines = new ArrayList<>();
|
||||
|
||||
private boolean mGotFix;
|
||||
|
||||
// User preferences for map rotation based on sensors
|
||||
private boolean mRotate;
|
||||
|
||||
private Location mLastLocation;
|
||||
|
||||
private OnMapClickListener mOnMapClickListener;
|
||||
|
||||
MapViewModelController mMapController;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
Configuration.getInstance().load(Application.get(), PreferenceManager.getDefaultSharedPreferences(Application.get()));
|
||||
mMap = new MapView(inflater.getContext());
|
||||
mMap.setMultiTouchControls(true);
|
||||
mMap.setBuiltInZoomControls(false);
|
||||
mMap.getController().setZoom(3.0f);
|
||||
|
||||
mRotationGestureOverlay = new RotationGestureOverlay(mMap);
|
||||
mRotationGestureOverlay.setEnabled(true);
|
||||
mMap.getOverlays().add(mRotationGestureOverlay);
|
||||
|
||||
mLastLocation = null;
|
||||
|
||||
mMapController = new MapViewModelController(getActivity(), this);
|
||||
mMapController.restoreState(savedInstanceState, getArguments(), mGroundTruthMarker == null);
|
||||
mMap.invalidate();
|
||||
|
||||
addMapClickListener();
|
||||
|
||||
GpsTestActivity.getInstance().addListener(this);
|
||||
|
||||
return mMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
SharedPreferences settings = Application.getPrefs();
|
||||
if (mMap != null && mMapController.getMode().equals(MODE_MAP)) {
|
||||
try {
|
||||
setMapBoxTileSource(MAP_TYPE_STREETS);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Log.e(mMapController.getMode(), "Error setting tile source: " + e);
|
||||
}
|
||||
} else if (mMap != null && mMapController.getMode().equals(MODE_ACCURACY)) {
|
||||
try {
|
||||
setMapBoxTileSource(MAP_TYPE_SATELLITE);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Log.e(mMapController.getMode(), "Error setting tile source: " + e);
|
||||
}
|
||||
}
|
||||
if (mMapController.getMode().equals(MODE_MAP)) {
|
||||
mRotate = settings
|
||||
.getBoolean(getString(R.string.pref_key_rotate_map_with_compass), true);
|
||||
}
|
||||
|
||||
mMap.onResume();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener that should receive map click events
|
||||
* @param listener the listener that should receive map click events
|
||||
*/
|
||||
public void setOnMapClickListener(OnMapClickListener listener) {
|
||||
mOnMapClickListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle bundle) {
|
||||
bundle.putString(MODE, mMapController.getMode());
|
||||
bundle.putBoolean(ALLOW_GROUND_TRUTH_CHANGE, mMapController.allowGroundTruthChange());
|
||||
if (mMapController.getGroundTruthLocation() != null) {
|
||||
bundle.putParcelable(GROUND_TRUTH, mMapController.getGroundTruthLocation());
|
||||
}
|
||||
super.onSaveInstanceState(bundle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
mMap.onPause();
|
||||
}
|
||||
|
||||
private void addMapClickListener() {
|
||||
final MapEventsReceiver mReceive = new MapEventsReceiver(){
|
||||
@Override
|
||||
public boolean singleTapConfirmedHelper(GeoPoint p) {
|
||||
if (!mMapController.getMode().equals(MODE_ACCURACY) || !mMapController.allowGroundTruthChange()) {
|
||||
// Don't allow changes to the ground truth location, so don't pass taps to listener
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mMap != null) {
|
||||
addGroundTruthMarker(MapUtils.makeLocation(p));
|
||||
mMap.invalidate();
|
||||
}
|
||||
|
||||
if (mOnMapClickListener != null) {
|
||||
Location location = new Location("OnMapClick");
|
||||
location.setLatitude(p.getLatitude());
|
||||
location.setLongitude(p.getLongitude());
|
||||
mOnMapClickListener.onMapClick(location);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public boolean longPressHelper(GeoPoint p) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
mMap.getOverlays().add(new MapEventsOverlay(mReceive));
|
||||
}
|
||||
|
||||
private void setMapBoxTileSource(String mapType) throws UnsupportedEncodingException {
|
||||
// To prevent web scrapers from easily finding the key, we store it encoded
|
||||
final String keyBase64 = "amdXY2VockFndXc2R1R1U3dQTmk=";
|
||||
final String key = MathUtils.fromBase64(keyBase64);
|
||||
|
||||
final ITileSource tileSource;
|
||||
if (mapType.equals(MAP_TYPE_SATELLITE)) {
|
||||
// Use the Maptiler format
|
||||
tileSource = new OnlineTileSourceBase("Maptiler Satellite Hybrid", 1, 19, 256, "",
|
||||
new String[]{"https://api.maptiler.com/maps/hybrid/"}) {
|
||||
@Override
|
||||
public String getTileURLString(long pMapTileIndex) {
|
||||
return getBaseUrl()
|
||||
+ MapTileIndex.getZoom(pMapTileIndex)
|
||||
+ "/" + MapTileIndex.getX(pMapTileIndex)
|
||||
+ "/" + MapTileIndex.getY(pMapTileIndex)
|
||||
+ "@2x.jpg?key=" + key;
|
||||
}
|
||||
};
|
||||
mMap.setTileSource(tileSource);
|
||||
} else {
|
||||
// Below is commented out due to Mapbox billing - until this is resolved, use default OSMDroid tiles
|
||||
|
||||
// We're using a Mapbox style, which isn't directly supported by OSMDroid due to a different URL format than Map IDs, so build the URL ourselves
|
||||
// tileSource = new OnlineTileSourceBase("MapBox Streets", 1, 19, 256, "",
|
||||
// new String[] { "https://api.mapbox.com/styles/v1/" + MAP_TYPE_STREETS + "/tiles/256/"}) {
|
||||
// @Override
|
||||
// public String getTileURLString(long pMapTileIndex) {
|
||||
// return getBaseUrl()
|
||||
// + MapTileIndex.getZoom(pMapTileIndex)
|
||||
// + "/" + MapTileIndex.getX(pMapTileIndex)
|
||||
// + "/" + MapTileIndex.getY(pMapTileIndex)
|
||||
// + "@2x?access_token=" + key;
|
||||
// }
|
||||
// };
|
||||
// mMap.setTileSource(tileSource);
|
||||
}
|
||||
}
|
||||
|
||||
public void gpsStart() {
|
||||
mGotFix = false;
|
||||
}
|
||||
|
||||
public void gpsStop() {
|
||||
}
|
||||
|
||||
public void onLocationChanged(Location loc) {
|
||||
if (mMap == null || !mMap.isLayoutOccurred()) {
|
||||
return;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !mMap.isLaidOut()) {
|
||||
return;
|
||||
}
|
||||
GeoPoint startPoint = new GeoPoint(loc.getLatitude(), loc.getLongitude());
|
||||
if (!mGotFix) {
|
||||
// Zoom levels are a little different than Google Maps, so add 2 to our Google default to get the same view
|
||||
mMap.getController().setZoom(CAMERA_INITIAL_ZOOM + 2);
|
||||
mMap.getController().setCenter(startPoint);
|
||||
mGotFix = true;
|
||||
}
|
||||
|
||||
if (loc.hasAccuracy()) {
|
||||
// Add horizontal accuracy uncertainty as polygon
|
||||
if (mHorAccPolygon == null) {
|
||||
mHorAccPolygon = new Polygon();
|
||||
}
|
||||
ArrayList<GeoPoint> circle = Polygon.pointsAsCircle(startPoint, loc.getAccuracy());
|
||||
if (circle != null) {
|
||||
mHorAccPolygon.setPoints(circle);
|
||||
|
||||
if (!mMap.getOverlays().contains(mHorAccPolygon)) {
|
||||
mHorAccPolygon.setStrokeWidth(0.5f);
|
||||
mHorAccPolygon.setOnClickListener((polygon, mapView, eventPos) -> {
|
||||
// Disable clicks
|
||||
return false;
|
||||
});
|
||||
mHorAccPolygon.setFillColor(ContextCompat.getColor(Application.get(), R.color.horizontal_accuracy));
|
||||
mMap.getOverlays().add(mHorAccPolygon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mMapController.getMode().equals(MODE_ACCURACY) && mLastLocation != null) {
|
||||
// Draw line between this and last location
|
||||
boolean drawn = drawPathLine(mLastLocation, loc);
|
||||
if (drawn) {
|
||||
mLastLocation = loc;
|
||||
}
|
||||
}
|
||||
if (mMapController.getMode().equals(MODE_ACCURACY) && !mMapController.allowGroundTruthChange() && mMapController.getGroundTruthLocation() != null) {
|
||||
// Draw error line between ground truth and calculated position
|
||||
GeoPoint gt = MapUtils.makeGeoPoint(mMapController.getGroundTruthLocation());
|
||||
GeoPoint current = MapUtils.makeGeoPoint(loc);
|
||||
List<GeoPoint> points = new ArrayList<>(Arrays.asList(gt, current));
|
||||
|
||||
if (mErrorLine == null) {
|
||||
mErrorLine = new Polyline();
|
||||
mErrorLine.setColor(Color.WHITE);
|
||||
mErrorLine.setPoints(points);
|
||||
mMap.getOverlayManager().add(mErrorLine);
|
||||
} else {
|
||||
mErrorLine.setPoints(points);
|
||||
}
|
||||
}
|
||||
// Draw my location marker last so it's on top
|
||||
if (mMyLocationMarker == null) {
|
||||
mMyLocationMarker = new Marker(mMap);
|
||||
}
|
||||
|
||||
mMyLocationMarker.setPosition(startPoint);
|
||||
|
||||
if (!mMap.getOverlays().contains(mMyLocationMarker)) {
|
||||
// This is the first fix when this fragment is active
|
||||
mMyLocationMarker.setIcon(ContextCompat.getDrawable(Application.get(), R.drawable.my_location));
|
||||
mMyLocationMarker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER);
|
||||
mMap.getOverlays().remove(mMyLocationMarker);
|
||||
mMap.getOverlays().add(mMyLocationMarker);
|
||||
}
|
||||
if (mLastLocation == null) {
|
||||
mLastLocation = loc;
|
||||
}
|
||||
mMap.invalidate();
|
||||
}
|
||||
|
||||
public void onStatusChanged(String provider, int status, Bundle extras) {
|
||||
}
|
||||
|
||||
public void onProviderEnabled(String provider) {
|
||||
}
|
||||
|
||||
public void onProviderDisabled(String provider) {
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void onGpsStatusChanged(int event, GpsStatus status) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssFirstFix(int ttffMillis) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssFixAcquired() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssFixLost() {
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@Override
|
||||
public void onSatelliteStatusChanged(GnssStatus status) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssStarted() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssStopped() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGnssMeasurementsReceived(GnssMeasurementsEvent event) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNmeaMessage(String message, long timestamp) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOrientationChanged(double orientation, double tilt) {
|
||||
// For performance reasons, only proceed if this fragment is visible
|
||||
if (!getUserVisibleHint()) {
|
||||
return;
|
||||
}
|
||||
if (mMap == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
If we're in map mode, we have a location fix, and we have a preference to rotate the map based on sensors,
|
||||
then do the map camera reposition
|
||||
*/
|
||||
if (mMapController.getMode().equals(MODE_MAP) && mMyLocationMarker != null && mRotate) {
|
||||
mMap.setMapOrientation((float) -orientation);
|
||||
}
|
||||
mMap.invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addGroundTruthMarker(Location location) {
|
||||
if (mMap == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mGroundTruthMarker == null) {
|
||||
mGroundTruthMarker = new Marker(mMap);
|
||||
}
|
||||
|
||||
mGroundTruthMarker.setPosition(MapUtils.makeGeoPoint(location));
|
||||
mGroundTruthMarker.setIcon(ContextCompat.getDrawable(Application.get(), R.drawable.ic_ground_truth));
|
||||
mGroundTruthMarker.setTitle(Application.get().getString(R.string.ground_truth_marker_title));
|
||||
mGroundTruthMarker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM);
|
||||
|
||||
if (!mMap.getOverlays().contains(mGroundTruthMarker)) {
|
||||
mMap.getOverlays().add(mGroundTruthMarker);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a line on the map between the two locations if its greater than a threshold value defined
|
||||
* by DRAW_LINE_THRESHOLD_METERS
|
||||
* @param loc1
|
||||
* @param loc2
|
||||
*/
|
||||
@Override
|
||||
public boolean drawPathLine(Location loc1, Location loc2) {
|
||||
if (loc1.distanceTo(loc2) < DRAW_LINE_THRESHOLD_METERS) {
|
||||
return false;
|
||||
}
|
||||
Polyline line = new Polyline();
|
||||
List<GeoPoint> points = Arrays.asList(MapUtils.makeGeoPoint(loc1), MapUtils.makeGeoPoint(loc2));
|
||||
line.setPoints(points);
|
||||
line.setColor(Color.RED);
|
||||
line.setWidth(2.0f);
|
||||
mMap.getOverlayManager().add(line);
|
||||
|
||||
mPathLines.add(line);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all path lines from the map
|
||||
*/
|
||||
@Override
|
||||
public void removePathLines() {
|
||||
for (Polyline line : mPathLines) {
|
||||
mMap.getOverlayManager().remove(line);
|
||||
}
|
||||
mPathLines = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
452
GPSTest/src/osmdroid/java/com/android/gpstest/ui/MapFragment.kt
Normal file
@@ -0,0 +1,452 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
package com.android.gpstest.ui
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Color
|
||||
import android.location.Location
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.android.gpstest.Application
|
||||
import com.android.gpstest.R
|
||||
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 dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.osmdroid.config.Configuration
|
||||
import org.osmdroid.events.MapEventsReceiver
|
||||
import org.osmdroid.tileprovider.tilesource.ITileSource
|
||||
import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase
|
||||
import org.osmdroid.util.GeoPoint
|
||||
import org.osmdroid.util.MapTileIndex
|
||||
import org.osmdroid.views.MapView
|
||||
import org.osmdroid.views.overlay.MapEventsOverlay
|
||||
import org.osmdroid.views.overlay.Marker
|
||||
import org.osmdroid.views.overlay.Polygon
|
||||
import org.osmdroid.views.overlay.Polyline
|
||||
import org.osmdroid.views.overlay.gestures.RotationGestureOverlay
|
||||
import java.io.UnsupportedEncodingException
|
||||
import javax.inject.Inject
|
||||
import org.osmdroid.views.overlay.CopyrightOverlay
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MapFragment : Fragment(), MapInterface {
|
||||
private var map: MapView? = null
|
||||
var rotationGestureOverlay: RotationGestureOverlay? = null
|
||||
var myLocationMarker: Marker? = null
|
||||
var groundTruthMarker: Marker? = null
|
||||
var horAccPolygon: Polygon? = null
|
||||
var errorLine: Polyline? = null
|
||||
var pathLines: MutableList<Polyline> = ArrayList()
|
||||
private var gotFix = false
|
||||
|
||||
// User preferences for map rotation based on sensors
|
||||
private var rotate = false
|
||||
private var lastLocation: Location? = null
|
||||
private var onMapClickListener: OnMapClickListener? = null
|
||||
var mapController: MapViewModelController? = null
|
||||
|
||||
// Repository of location data that the service will observe, injected via Hilt
|
||||
@Inject
|
||||
lateinit var repository: LocationRepository
|
||||
|
||||
// Get a reference to the Job from the Flow so we can stop it from UI events
|
||||
private var locationFlow: Job? = null
|
||||
private var sensorFlow: Job? = null
|
||||
|
||||
// Preference listener that will cancel the above flows when the user turns off tracking via UI
|
||||
private val trackingListener: SharedPreferences.OnSharedPreferenceChangeListener =
|
||||
newStopTrackingListener ({ onGnssStopped() }, Application.prefs)
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
Configuration.getInstance().load(
|
||||
Application.app, PreferenceManager.getDefaultSharedPreferences(
|
||||
Application.app
|
||||
)
|
||||
)
|
||||
val map = MapView(inflater.context)
|
||||
this.map = map
|
||||
map.setMultiTouchControls(true)
|
||||
map.setBuiltInZoomControls(false)
|
||||
map.controller.setZoom(3.0)
|
||||
rotationGestureOverlay = RotationGestureOverlay(map)
|
||||
rotationGestureOverlay!!.isEnabled = true
|
||||
map.overlays.apply {
|
||||
add(rotationGestureOverlay)
|
||||
add(CopyrightOverlay(inflater.context))
|
||||
}
|
||||
lastLocation = null
|
||||
mapController = MapViewModelController(activity, this)
|
||||
mapController!!.restoreState(savedInstanceState, arguments, groundTruthMarker == null)
|
||||
map.invalidate()
|
||||
|
||||
Application.prefs.registerOnSharedPreferenceChangeListener(trackingListener)
|
||||
|
||||
addMapClickListener()
|
||||
observeLocationUpdateStates()
|
||||
return map
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val settings = Application.prefs
|
||||
if (map != null && mapController!!.mode == MapConstants.MODE_MAP) {
|
||||
try {
|
||||
setMapBoxTileSource(MAP_TYPE_STREETS)
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
Log.e(mapController!!.mode, "Error setting tile source: $e")
|
||||
}
|
||||
} else if (map != null && mapController!!.mode == MapConstants.MODE_ACCURACY) {
|
||||
try {
|
||||
setMapBoxTileSource(MAP_TYPE_SATELLITE)
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
Log.e(mapController!!.mode, "Error setting tile source: $e")
|
||||
}
|
||||
}
|
||||
if (mapController!!.mode == MapConstants.MODE_MAP) {
|
||||
rotate = settings
|
||||
.getBoolean(getString(R.string.pref_key_rotate_map_with_compass), true)
|
||||
}
|
||||
map!!.onResume()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener that should receive map click events
|
||||
* @param listener the listener that should receive map click events
|
||||
*/
|
||||
fun setOnMapClickListener(listener: OnMapClickListener?) {
|
||||
onMapClickListener = listener
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(bundle: Bundle) {
|
||||
bundle.putString(MapConstants.MODE, mapController!!.mode)
|
||||
bundle.putBoolean(
|
||||
MapConstants.ALLOW_GROUND_TRUTH_CHANGE,
|
||||
mapController!!.allowGroundTruthChange()
|
||||
)
|
||||
if (mapController!!.groundTruthLocation != null) {
|
||||
bundle.putParcelable(MapConstants.GROUND_TRUTH, mapController!!.groundTruthLocation)
|
||||
}
|
||||
super.onSaveInstanceState(bundle)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
map!!.onPause()
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeLocationUpdateStates() {
|
||||
repository.receivingLocationUpdates
|
||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
||||
.onEach {
|
||||
when (it) {
|
||||
true -> onGnssStarted()
|
||||
false -> onGnssStopped()
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private fun addMapClickListener() {
|
||||
val mReceive: MapEventsReceiver = object : MapEventsReceiver {
|
||||
override fun singleTapConfirmedHelper(p: GeoPoint): Boolean {
|
||||
if (mapController!!.mode != MapConstants.MODE_ACCURACY || !mapController!!.allowGroundTruthChange()) {
|
||||
// Don't allow changes to the ground truth location, so don't pass taps to listener
|
||||
return false
|
||||
}
|
||||
if (map != null) {
|
||||
addGroundTruthMarker(MapUtils.makeLocation(p))
|
||||
map!!.invalidate()
|
||||
}
|
||||
if (onMapClickListener != null) {
|
||||
val location = Location("OnMapClick")
|
||||
location.latitude = p.latitude
|
||||
location.longitude = p.longitude
|
||||
onMapClickListener!!.onMapClick(location)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun longPressHelper(p: GeoPoint): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
map!!.overlays.add(MapEventsOverlay(mReceive))
|
||||
}
|
||||
|
||||
@Throws(UnsupportedEncodingException::class)
|
||||
private fun setMapBoxTileSource(mapType: String) {
|
||||
// To prevent web scrapers from easily finding the key, we store it encoded
|
||||
val keyBase64 = "amdXY2VockFndXc2R1R1U3dQTmk="
|
||||
val key = MathUtils.fromBase64(keyBase64)
|
||||
val tileSource: ITileSource
|
||||
if (mapType == MAP_TYPE_SATELLITE) {
|
||||
// Use the Maptiler format
|
||||
tileSource = object : OnlineTileSourceBase(
|
||||
"Maptiler Satellite Hybrid",
|
||||
1,
|
||||
19,
|
||||
256,
|
||||
"",
|
||||
arrayOf("https://api.maptiler.com/maps/hybrid/")
|
||||
) {
|
||||
override fun getTileURLString(pMapTileIndex: Long): String {
|
||||
return (baseUrl
|
||||
+ MapTileIndex.getZoom(pMapTileIndex)
|
||||
+ "/" + MapTileIndex.getX(pMapTileIndex)
|
||||
+ "/" + MapTileIndex.getY(pMapTileIndex)
|
||||
+ "@2x.jpg?key=" + key)
|
||||
}
|
||||
|
||||
override fun getCopyrightNotice(): String {
|
||||
return "© MapTiler © OpenStreetMap contributors"
|
||||
}
|
||||
}
|
||||
map!!.setTileSource(tileSource)
|
||||
} else {
|
||||
// Below is commented out due to Mapbox billing - until this is resolved, use default OSMDroid tiles
|
||||
|
||||
// We're using a Mapbox style, which isn't directly supported by OSMDroid due to a different URL format than Map IDs, so build the URL ourselves
|
||||
// tileSource = new OnlineTileSourceBase("MapBox Streets", 1, 19, 256, "",
|
||||
// new String[] { "https://api.mapbox.com/styles/v1/" + MAP_TYPE_STREETS + "/tiles/256/"}) {
|
||||
// @Override
|
||||
// public String getTileURLString(long pMapTileIndex) {
|
||||
// return getBaseUrl()
|
||||
// + MapTileIndex.getZoom(pMapTileIndex)
|
||||
// + "/" + MapTileIndex.getX(pMapTileIndex)
|
||||
// + "/" + MapTileIndex.getY(pMapTileIndex)
|
||||
// + "@2x?access_token=" + key;
|
||||
// }
|
||||
// };
|
||||
// mMap.setTileSource(tileSource);
|
||||
}
|
||||
}
|
||||
|
||||
private fun onGnssStarted() {
|
||||
gotFix = false
|
||||
observeFlows()
|
||||
}
|
||||
|
||||
private fun onGnssStopped() {
|
||||
// Cancel updates (Note that these are canceled via scope in main Activity too,
|
||||
// otherwise updates won't stop because this Fragment doesn't get the switch UI event.
|
||||
// But cancel() here too for good practice)
|
||||
locationFlow?.cancel()
|
||||
sensorFlow?.cancel()
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeFlows() {
|
||||
observeLocationFlow()
|
||||
observeSensorFlow()
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeLocationFlow() {
|
||||
if (locationFlow?.isActive == true) {
|
||||
// If we're already observing updates, don't register again
|
||||
return
|
||||
}
|
||||
// Observe locations via Flow as they are generated by the repository
|
||||
locationFlow = repository.getLocations()
|
||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
||||
.onEach {
|
||||
//Log.d(GpsStatusFragment.TAG, "Map location: ${it.toNotificationTitle()}")
|
||||
onLocationChanged(it)
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeSensorFlow() {
|
||||
if (sensorFlow?.isActive == true) {
|
||||
// If we're already observing updates, don't register again
|
||||
return
|
||||
}
|
||||
// Observe locations via Flow as they are generated by the repository
|
||||
sensorFlow = repository.getSensorUpdates()
|
||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
||||
.onEach {
|
||||
//Log.d(TAG, "Map sensor: orientation ${it.values[0]}, tilt ${it.values[1]}")
|
||||
onOrientationChanged(it.values[0], it.values[1])
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private fun onLocationChanged(loc: Location) {
|
||||
if (map == null || !map!!.isLayoutOccurred || !map!!.isLaidOut) {
|
||||
return
|
||||
}
|
||||
val startPoint = GeoPoint(loc.latitude, loc.longitude)
|
||||
if (!gotFix) {
|
||||
// Zoom levels are a little different than Google Maps, so add 2 to our Google default to get the same view
|
||||
map!!.controller.setZoom((MapConstants.CAMERA_INITIAL_ZOOM + 2).toDouble())
|
||||
map!!.controller.setCenter(startPoint)
|
||||
gotFix = true
|
||||
}
|
||||
if (loc.hasAccuracy()) {
|
||||
// Add horizontal accuracy uncertainty as polygon
|
||||
if (horAccPolygon == null) {
|
||||
horAccPolygon = Polygon()
|
||||
}
|
||||
val circle = Polygon.pointsAsCircle(startPoint, loc.accuracy.toDouble())
|
||||
if (circle != null) {
|
||||
horAccPolygon!!.points = circle
|
||||
if (!map!!.overlays.contains(horAccPolygon)) {
|
||||
horAccPolygon!!.strokeWidth = 0.5f
|
||||
horAccPolygon!!.setOnClickListener { _: Polygon?, _: MapView?, _: GeoPoint? -> false }
|
||||
horAccPolygon!!.fillColor =
|
||||
ContextCompat.getColor(Application.app, R.color.horizontal_accuracy)
|
||||
map!!.overlays.add(horAccPolygon)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mapController!!.mode == MapConstants.MODE_ACCURACY && lastLocation != null) {
|
||||
// Draw line between this and last location
|
||||
val drawn = drawPathLine(lastLocation!!, loc)
|
||||
if (drawn) {
|
||||
lastLocation = loc
|
||||
}
|
||||
}
|
||||
if (mapController!!.mode == MapConstants.MODE_ACCURACY && !mapController!!.allowGroundTruthChange() && mapController!!.groundTruthLocation != null) {
|
||||
// Draw error line between ground truth and calculated position
|
||||
val gt = MapUtils.makeGeoPoint(mapController!!.groundTruthLocation)
|
||||
val current = MapUtils.makeGeoPoint(loc)
|
||||
val points: List<GeoPoint> = listOf(gt, current)
|
||||
if (errorLine == null) {
|
||||
errorLine = Polyline()
|
||||
errorLine!!.color = Color.WHITE
|
||||
errorLine!!.setPoints(points)
|
||||
map!!.overlayManager.add(errorLine)
|
||||
} else {
|
||||
errorLine!!.setPoints(points)
|
||||
}
|
||||
}
|
||||
// Draw my location marker last so it's on top
|
||||
if (myLocationMarker == null) {
|
||||
myLocationMarker = Marker(map)
|
||||
}
|
||||
myLocationMarker!!.position = startPoint
|
||||
if (!map!!.overlays.contains(myLocationMarker)) {
|
||||
// This is the first fix when this fragment is active
|
||||
myLocationMarker!!.icon =
|
||||
ContextCompat.getDrawable(Application.app, R.drawable.my_location)
|
||||
myLocationMarker!!.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
|
||||
map!!.overlays.remove(myLocationMarker)
|
||||
map!!.overlays.add(myLocationMarker)
|
||||
}
|
||||
if (lastLocation == null) {
|
||||
lastLocation = loc
|
||||
}
|
||||
map!!.invalidate()
|
||||
}
|
||||
|
||||
private fun onOrientationChanged(orientation: Double, tilt: Double) {
|
||||
// For performance reasons, only proceed if this fragment is visible
|
||||
if (!userVisibleHint) {
|
||||
return
|
||||
}
|
||||
if (map == null) {
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
If we're in map mode, we have a location fix, and we have a preference to rotate the map based on sensors,
|
||||
then do the map camera reposition
|
||||
*/if (mapController!!.mode == MapConstants.MODE_MAP && myLocationMarker != null && rotate) {
|
||||
map!!.mapOrientation = (-orientation).toFloat()
|
||||
}
|
||||
map!!.invalidate()
|
||||
}
|
||||
|
||||
override fun addGroundTruthMarker(location: Location) {
|
||||
if (map == null) {
|
||||
return
|
||||
}
|
||||
if (groundTruthMarker == null) {
|
||||
groundTruthMarker = Marker(map)
|
||||
}
|
||||
groundTruthMarker!!.position = MapUtils.makeGeoPoint(location)
|
||||
groundTruthMarker!!.icon =
|
||||
ContextCompat.getDrawable(Application.app, R.drawable.ic_ground_truth)
|
||||
groundTruthMarker!!.title = Application.app.getString(R.string.ground_truth_marker_title)
|
||||
groundTruthMarker!!.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||
if (!map!!.overlays.contains(groundTruthMarker)) {
|
||||
map!!.overlays.add(groundTruthMarker)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a line on the map between the two locations if its greater than a threshold value defined
|
||||
* by DRAW_LINE_THRESHOLD_METERS
|
||||
* @param loc1
|
||||
* @param loc2
|
||||
*/
|
||||
override fun drawPathLine(loc1: Location, loc2: Location): Boolean {
|
||||
if (loc1.distanceTo(loc2) < MapConstants.DRAW_LINE_THRESHOLD_METERS) {
|
||||
return false
|
||||
}
|
||||
val line = Polyline()
|
||||
val points = listOf(MapUtils.makeGeoPoint(loc1), MapUtils.makeGeoPoint(loc2))
|
||||
line.setPoints(points)
|
||||
line.color = Color.RED
|
||||
line.width = 2.0f
|
||||
map!!.overlayManager.add(line)
|
||||
pathLines.add(line)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all path lines from the map
|
||||
*/
|
||||
override fun removePathLines() {
|
||||
for (line in pathLines) {
|
||||
map!!.overlayManager.remove(line)
|
||||
}
|
||||
pathLines = ArrayList()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "GpsMapFragment"
|
||||
private const val MAP_TYPE_SATELLITE = "mapbox.satellite"
|
||||
private const val MAP_TYPE_STREETS = "barbeau/cju1g27421a0w1fmvsy13tjfv"
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,9 @@
|
||||
*/
|
||||
package com.android.gpstest.util
|
||||
|
||||
import junit.framework.Assert.assertFalse
|
||||
import junit.framework.Assert.assertTrue
|
||||
import com.android.gpstest.library.util.DateTimeUtils
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -15,12 +15,14 @@
|
||||
*/
|
||||
package com.android.gpstest.util;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
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 {
|
||||
|
||||
/**
|
||||
@@ -28,8 +30,8 @@ public class MathUtilTest {
|
||||
*/
|
||||
@Test
|
||||
public void testToMhz() {
|
||||
float mhz = MathUtils.toMhz(1000000.0f);
|
||||
assertEquals(1.0f, mhz);
|
||||
double mhz = MathUtils.toMhz(1000000.0);
|
||||
assertEquals(1.0, mhz);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,13 +15,14 @@
|
||||
*/
|
||||
package com.android.gpstest.util;
|
||||
|
||||
import com.android.gpstest.model.DilutionOfPrecision;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
import static junit.framework.Assert.assertNull;
|
||||
|
||||
import com.android.gpstest.library.model.DilutionOfPrecision;
|
||||
import com.android.gpstest.library.util.NmeaUtils;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class NmeaUtilsTest {
|
||||
/**
|
||||
* Test getting altitude above mean sea level (geoid) from NMEA sentences
|
||||
|
||||