Compare commits

...

94 Commits

Author SHA1 Message Date
dependabot[bot]
90e0d9f9be actions-update: Bump the actions-dependencies group across 1 directory with 3 updates (#763)
Bumps the actions-dependencies group with 3 updates in the / directory: [step-security/harden-runner](https://github.com/step-security/harden-runner), [actions/setup-java](https://github.com/actions/setup-java) and [github/codeql-action](https://github.com/github/codeql-action).


Updates `step-security/harden-runner` from 2.13.2 to 2.13.3
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](95d9a5deda...df199fb7be)

Updates `actions/setup-java` from 5.0.0 to 5.1.0
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](dded088883...f2beeb24e1)

Updates `github/codeql-action` from 4.31.4 to 4.31.6
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](e12f017898...fe4161a26a)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-version: 2.13.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-dependencies
- dependency-name: actions/setup-java
  dependency-version: 5.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-dependencies
- dependency-name: github/codeql-action
  dependency-version: 4.31.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-05 17:22:09 -05:00
dependabot[bot]
80b6848a09 actions-update: Bump the actions-dependencies group across 1 directory with 4 updates (#760)
Bumps the actions-dependencies group with 4 updates in the / directory: [step-security/harden-runner](https://github.com/step-security/harden-runner), [actions/checkout](https://github.com/actions/checkout), [github/codeql-action](https://github.com/github/codeql-action) and [reactivecircus/android-emulator-runner](https://github.com/reactivecircus/android-emulator-runner).


Updates `step-security/harden-runner` from 2.13.1 to 2.13.2
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](f4a75cfd61...95d9a5deda)

Updates `actions/checkout` from 5 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

Updates `github/codeql-action` from 4.31.0 to 4.31.4
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](4e94bd11f7...e12f017898)

Updates `reactivecircus/android-emulator-runner` from 2.34.0 to 2.35.0
- [Release notes](https://github.com/reactivecircus/android-emulator-runner/releases)
- [Changelog](https://github.com/ReactiveCircus/android-emulator-runner/blob/main/CHANGELOG.md)
- [Commits](1dcd009011...b530d96654)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-version: 2.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-dependencies
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-dependencies
- dependency-name: github/codeql-action
  dependency-version: 4.31.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-dependencies
- dependency-name: reactivecircus/android-emulator-runner
  dependency-version: 2.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-22 21:20:45 -05:00
dependabot[bot]
c5a3d684e6 actions-update: Bump the actions-dependencies group across 1 directory with 3 updates (#755)
Bumps the actions-dependencies group with 3 updates in the / directory: [gradle/actions](https://github.com/gradle/actions), [github/codeql-action](https://github.com/github/codeql-action) and [actions/upload-artifact](https://github.com/actions/upload-artifact).


Updates `gradle/actions` from 4.4.3 to 5.0.0
- [Release notes](https://github.com/gradle/actions/releases)
- [Commits](ed408507ea...4d9f0ba002)

Updates `github/codeql-action` from 3.30.3 to 4.31.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](192325c861...4e94bd11f7)

Updates `actions/upload-artifact` from 4.6.2 to 5.0.0
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](ea165f8d65...330a01c490)

---
updated-dependencies:
- dependency-name: gradle/actions
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-dependencies
- dependency-name: github/codeql-action
  dependency-version: 4.31.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-dependencies
- dependency-name: actions/upload-artifact
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 07:44:21 -04:00
dependabot[bot]
f549508ad3 actions-update: Bump actions/cache in the actions-dependencies group (#751)
Bumps the actions-dependencies group with 1 update: [actions/cache](https://github.com/actions/cache).


Updates `actions/cache` from 4.2.4 to 4.3.0
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](0400d5f644...0057852bfa)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-24 23:14:29 -04:00
dependabot[bot]
a3dfa50b99 actions-update: Bump github/codeql-action (#750)
Bumps the actions-dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 3.30.2 to 3.30.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](d3678e237b...192325c861)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.30.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-10 21:49:25 -04:00
dependabot[bot]
640bd4b9a7 actions-update: Bump the actions-dependencies group with 3 updates (#749)
Bumps the actions-dependencies group with 3 updates: [step-security/harden-runner](https://github.com/step-security/harden-runner), [gradle/actions](https://github.com/gradle/actions) and [github/codeql-action](https://github.com/github/codeql-action).


Updates `step-security/harden-runner` from 2.13.0 to 2.13.1
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](ec9f2d5744...f4a75cfd61)

Updates `gradle/actions` from 4.4.2 to 4.4.3
- [Release notes](https://github.com/gradle/actions/releases)
- [Commits](017a9effdb...ed408507ea)

Updates `github/codeql-action` from 3.30.1 to 3.30.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](f1f6e5f6af...d3678e237b)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-version: 2.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-dependencies
- dependency-name: gradle/actions
  dependency-version: 4.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-dependencies
- dependency-name: github/codeql-action
  dependency-version: 3.30.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-09 22:32:28 -04:00
dependabot[bot]
2b1ed93629 actions-update: Bump the actions-dependencies group with 2 updates (#748)
Bumps the actions-dependencies group with 2 updates: [actions/setup-java](https://github.com/actions/setup-java) and [github/codeql-action](https://github.com/github/codeql-action).


Updates `actions/setup-java` from 4.7.1 to 5.0.0
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](c5195efecf...dded088883)

Updates `github/codeql-action` from 3.29.10 to 3.29.11
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](96f518a34f...3c3833e0f8)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-dependencies
- dependency-name: github/codeql-action
  dependency-version: 3.29.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-07 22:01:48 -04:00
Christian Rowlands
148b609e37 fix: Sets the correct color for the SouthPAN Legend diamond and dashes (#681)
Adds the diamond and dashes to the list of shapes and lines so that the colors get updated appropriately when dark mode is turned on
2025-09-06 12:36:43 -04:00
Izzy
acea0e0249 fastlane: slightly improve formatting for full description (#739) 2025-09-05 09:42:25 -04:00
dependabot[bot]
9fd28d5a61 actions-update: Bump github/codeql-action (#747)
Bumps the actions-dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 3.29.9 to 3.29.10
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](df559355d5...96f518a34f)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.29.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-20 16:56:08 -04:00
dependabot[bot]
4ac38c380f actions-update: Bump github/codeql-action (#745)
Bumps the actions-dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 3.29.8 to 3.29.9
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](76621b61de...df559355d5)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.29.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-13 11:14:15 -04:00
dependabot[bot]
3b23f9514b actions-update: Bump actions/checkout in the actions-dependencies group (#744)
Bumps the actions-dependencies group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 4 to 5
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 09:16:12 -04:00
dependabot[bot]
726481d50c actions-update: Bump github/codeql-action (#743)
Bumps the actions-dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 3.29.7 to 3.29.8
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](51f77329af...76621b61de)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.29.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-10 23:37:39 -04:00
dependabot[bot]
c304bc8fdb actions-update: Bump the actions-dependencies group across 1 directory with 2 updates (#742)
Bumps the actions-dependencies group with 2 updates in the / directory: [gradle/actions](https://github.com/gradle/actions) and [actions/cache](https://github.com/actions/cache).


Updates `gradle/actions` from 4.4.1 to 4.4.2
- [Release notes](https://github.com/gradle/actions/releases)
- [Commits](ac638b010c...017a9effdb)

Updates `actions/cache` from 4.2.3 to 4.2.4
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](5a3ec84eff...0400d5f644)

---
updated-dependencies:
- dependency-name: gradle/actions
  dependency-version: 4.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-dependencies
- dependency-name: actions/cache
  dependency-version: 4.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-07 23:24:31 -04:00
dependabot[bot]
6eaabe2f8c actions-update: Bump github/codeql-action (#736)
Bumps the actions-dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 3.29.4 to 3.29.5
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](4e828ff8d4...51f77329af)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.29.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-29 21:45:21 -04:00
Florian Stosse
6994b33a8c chore: Upgrade Gradle Wrapper to v8.14.3 (#734) 2025-07-27 16:23:59 -04:00
Florian Stosse
25902ed60f build: fix linting (#728)
* Update android.yml

* Update android.yml

* Update AndroidManifest.xml

* Update android.yml

* Update AndroidManifest.xml

* Separate steps

* Avoid linting

* List packages

* Build debug

* Add stacktrace

* Upload artifacts

* Checkout

* Wait for build

* Fix ADB path

* Fix paths

* Update targets

* Test and check

* Fix path

* Improve pipeline

* Fix CI

* Fix action

* Upload on failure

* Removel inting

* Fix lint report path

* Fix path

* Print paths content

* Add params

* Fix path

* Remove lint

* Check

* Test only

* Format

* Re add linting

* Remove format

* Fix paths

* Uneeded

* Disable telemetry

* Fix library use

* Revert lib change

* Use Gradle to deploy

* Run instrumentation tests after install

* Save artifacts directories

* Run instrumented tests on main app only

* Manual SDK install

* connectedCheck

* Use runner action

* No manual install

* Temporary comment

* No artifact download

* Add Checkstyle

* Pin actions and remove comments

* Needs build

* Add Dependabot

* Harden runners

* Add checkstyle configuration

* Consistency

* actions-update: Bump actions/checkout in the actions-dependencies group

Bumps the actions-dependencies group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 2 to 4
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v2...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* Hardening in block mode

* build(deps): Bump the gradle group across 1 directory with 3 updates

Bumps the gradle group with 3 updates in the / directory: commons-io:commons-io, [com.fasterxml.jackson.core:jackson-core](https://github.com/FasterXML/jackson-core) and [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson).


Updates `commons-io:commons-io` from 2.8.0 to 2.14.0

Updates `com.fasterxml.jackson.core:jackson-core` from 2.14.2 to 2.15.0
- [Changelog](https://github.com/FasterXML/jackson-core/blob/jackson-core-2.15.0/release.properties)
- [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.14.2...jackson-core-2.15.0)

Updates `com.fasterxml.jackson.core:jackson-databind` from 2.12.4 to 2.12.7.1
- [Commits](https://github.com/FasterXML/jackson/commits)

---
updated-dependencies:
- dependency-name: commons-io:commons-io
  dependency-version: 2.14.0
  dependency-type: direct:production
  dependency-group: gradle
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.15.0
  dependency-type: direct:production
  dependency-group: gradle
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.12.7.1
  dependency-type: direct:production
  dependency-group: gradle
...

Signed-off-by: dependabot[bot] <support@github.com>

* Gradle 8.14

* Fix egress

* Fix egress

* Audit

* Revert

* Run checkstyle

* Add task

* Remove checkstyle

* Remove checkstyle

* Fix name

* Add name

* Correct reports path

* Print paths

* Print

* Upload build logs

* Remove logs upload

* Misc

* Cache readonly (#4)

* Implement read only cache

* Build OsmDroid

* Parallelize jobs (#6)

* actions-update: Bump step-security/harden-runner (#5)

Bumps the actions-dependencies group with 1 update: [step-security/harden-runner](https://github.com/step-security/harden-runner).


Updates `step-security/harden-runner` from 2.12.2 to 2.13.0
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](6c439dc8bd...ec9f2d5744)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-version: 2.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Upgrade to Gradle 8.13

* Upgrade Kotlin version

* Add buildConfig

* Optimize imports

* Dependabot: only upgrade at patch version level

* Revert Gradle update action

* Dependabot: add Gradle support for patch versions

* gradle-update: bump org.osmdroid:osmdroid-android (#7)

Bumps the actions-dependencies group with 1 update: [org.osmdroid:osmdroid-android](https://github.com/osmdroid/osmdroid).


Updates `org.osmdroid:osmdroid-android` from 6.1.11 to 6.1.20
- [Release notes](https://github.com/osmdroid/osmdroid/releases)
- [Changelog](https://github.com/osmdroid/osmdroid/blob/master/CHANGELOG.md)
- [Commits](https://github.com/osmdroid/osmdroid/compare/osmdroid-parent-6.1.11...osmdroid-parent-6.1.20)

---
updated-dependencies:
- dependency-name: org.osmdroid:osmdroid-android
  dependency-version: 6.1.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* gradle-update: bump com.fasterxml.jackson.core:jackson-databind (#8)

Bumps [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson) from 2.12.4 to 2.19.2.
- [Commits](https://github.com/FasterXML/jackson/commits)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.19.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Fix typo in Dependabot conf

* Bump SDK version

* Upgrade SDK version

* Revert targetSdk

* Bump SDK

* Fix type

* Fix build

* Security updates for Gradle

* Ignore major and minor updates

* Lint

* Lint: ignore string format

* Lint: ignore string format for library

* Lint: disable translation checks

* Migrate activity!!

* Migrate context!!

* Add SARIF report generation and upload

* Lint: do not check dependencies

* Fix path again

* Lint: re-enable for all tasks

* Add gitignore

* Bump targetSdk

* Remove Dependabot Gradle config

* Remove crashpad handler fix

* Bump SARIF action to latest version

* Revert SDK upgrade

* Revert SDK upgrade related fixes

* Revert non-lint fixes

* Revert sensor fix

* Restore gitignore

* Delete build/reports/problems/problems-report.html

* Fix pipeline run

* Add required fixes from linting pass

* Fix conflict

* Revert targetSdk upgrade and ignore ExpiredTargetSdkVersion for linting

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-27 09:58:10 -04:00
Florian Stosse
1eb6ed9477 chore: Add Android focused gitignore file (#732) 2025-07-26 12:39:17 -04:00
Florian Stosse
acc19cff01 ci: Terminate crashpad_handler process on emulator exit (#733) 2025-07-26 11:05:28 -04:00
Florian Stosse
8a700e987e fix: Fix XML closing element (#731) 2025-07-25 14:03:45 -04:00
Florian Stosse
4e7c77fbb2 chore: Enable caching for Gradle (#730) 2025-07-25 13:53:58 -04:00
Florian Stosse
91378d5f79 chore: Upgrade Gradle, Kotlin and optimize imports (#726)
* Update android.yml

* Update android.yml

* Update AndroidManifest.xml

* Update android.yml

* Update AndroidManifest.xml

* Separate steps

* Avoid linting

* List packages

* Build debug

* Add stacktrace

* Upload artifacts

* Checkout

* Wait for build

* Fix ADB path

* Fix paths

* Update targets

* Test and check

* Fix path

* Improve pipeline

* Fix CI

* Fix action

* Upload on failure

* Removel inting

* Fix lint report path

* Fix path

* Print paths content

* Add params

* Fix path

* Remove lint

* Check

* Test only

* Format

* Re add linting

* Remove format

* Fix paths

* Uneeded

* Disable telemetry

* Fix library use

* Revert lib change

* Use Gradle to deploy

* Run instrumentation tests after install

* Save artifacts directories

* Run instrumented tests on main app only

* Manual SDK install

* connectedCheck

* Use runner action

* No manual install

* Temporary comment

* No artifact download

* Add Checkstyle

* Pin actions and remove comments

* Needs build

* Add Dependabot

* Harden runners

* Add checkstyle configuration

* Consistency

* actions-update: Bump actions/checkout in the actions-dependencies group

Bumps the actions-dependencies group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 2 to 4
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v2...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* Hardening in block mode

* build(deps): Bump the gradle group across 1 directory with 3 updates

Bumps the gradle group with 3 updates in the / directory: commons-io:commons-io, [com.fasterxml.jackson.core:jackson-core](https://github.com/FasterXML/jackson-core) and [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson).


Updates `commons-io:commons-io` from 2.8.0 to 2.14.0

Updates `com.fasterxml.jackson.core:jackson-core` from 2.14.2 to 2.15.0
- [Changelog](https://github.com/FasterXML/jackson-core/blob/jackson-core-2.15.0/release.properties)
- [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.14.2...jackson-core-2.15.0)

Updates `com.fasterxml.jackson.core:jackson-databind` from 2.12.4 to 2.12.7.1
- [Commits](https://github.com/FasterXML/jackson/commits)

---
updated-dependencies:
- dependency-name: commons-io:commons-io
  dependency-version: 2.14.0
  dependency-type: direct:production
  dependency-group: gradle
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.15.0
  dependency-type: direct:production
  dependency-group: gradle
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.12.7.1
  dependency-type: direct:production
  dependency-group: gradle
...

Signed-off-by: dependabot[bot] <support@github.com>

* Gradle 8.14

* Fix egress

* Fix egress

* Audit

* Revert

* Run checkstyle

* Add task

* Remove checkstyle

* Remove checkstyle

* Fix name

* Add name

* Correct reports path

* Print paths

* Print

* Upload build logs

* Remove logs upload

* Misc

* Cache readonly (#4)

* Implement read only cache

* Build OsmDroid

* Parallelize jobs (#6)

* actions-update: Bump step-security/harden-runner (#5)

Bumps the actions-dependencies group with 1 update: [step-security/harden-runner](https://github.com/step-security/harden-runner).


Updates `step-security/harden-runner` from 2.12.2 to 2.13.0
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](6c439dc8bd...ec9f2d5744)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-version: 2.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Upgrade to Gradle 8.13

* Upgrade Kotlin version

* Add buildConfig

* Optimize imports

* Dependabot: only upgrade at patch version level

* Revert Gradle update action

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-20 12:56:11 -04:00
Florian Stosse
5d4a0f9a60 ci: Fix CI pipeline (#725)
* Update android.yml

* Update android.yml

* Update AndroidManifest.xml

* Update android.yml

* Update AndroidManifest.xml

* Separate steps

* Avoid linting

* List packages

* Build debug

* Add stacktrace

* Upload artifacts

* Checkout

* Wait for build

* Fix ADB path

* Fix paths

* Update targets

* Test and check

* Fix path

* Improve pipeline

* Fix CI

* Fix action

* Upload on failure

* Removel inting

* Fix lint report path

* Fix path

* Print paths content

* Add params

* Fix path

* Remove lint

* Check

* Test only

* Format

* Re add linting

* Remove format

* Fix paths

* Uneeded

* Disable telemetry

* Fix library use

* Revert lib change

* Use Gradle to deploy

* Run instrumentation tests after install

* Save artifacts directories

* Run instrumented tests on main app only

* Manual SDK install

* connectedCheck

* Use runner action

* No manual install

* Temporary comment

* No artifact download

* Add Checkstyle

* Pin actions and remove comments

* Needs build

* Add Dependabot

* Harden runners

* Add checkstyle configuration

* Consistency

* actions-update: Bump actions/checkout in the actions-dependencies group

Bumps the actions-dependencies group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 2 to 4
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v2...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* Hardening in block mode

* build(deps): Bump the gradle group across 1 directory with 3 updates

Bumps the gradle group with 3 updates in the / directory: commons-io:commons-io, [com.fasterxml.jackson.core:jackson-core](https://github.com/FasterXML/jackson-core) and [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson).


Updates `commons-io:commons-io` from 2.8.0 to 2.14.0

Updates `com.fasterxml.jackson.core:jackson-core` from 2.14.2 to 2.15.0
- [Changelog](https://github.com/FasterXML/jackson-core/blob/jackson-core-2.15.0/release.properties)
- [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.14.2...jackson-core-2.15.0)

Updates `com.fasterxml.jackson.core:jackson-databind` from 2.12.4 to 2.12.7.1
- [Commits](https://github.com/FasterXML/jackson/commits)

---
updated-dependencies:
- dependency-name: commons-io:commons-io
  dependency-version: 2.14.0
  dependency-type: direct:production
  dependency-group: gradle
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.15.0
  dependency-type: direct:production
  dependency-group: gradle
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.12.7.1
  dependency-type: direct:production
  dependency-group: gradle
...

Signed-off-by: dependabot[bot] <support@github.com>

* Gradle 8.14

* Fix egress

* Fix egress

* Audit

* Revert

* Run checkstyle

* Add task

* Remove checkstyle

* Remove checkstyle

* Fix name

* Add name

* Correct reports path

* Print paths

* Print

* Upload build logs

* Remove logs upload

* Misc

* Cache readonly (#4)

* Implement read only cache

* Build OsmDroid

* Parallelize jobs (#6)

* actions-update: Bump step-security/harden-runner (#5)

Bumps the actions-dependencies group with 1 update: [step-security/harden-runner](https://github.com/step-security/harden-runner).


Updates `step-security/harden-runner` from 2.12.2 to 2.13.0
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](6c439dc8bd...ec9f2d5744)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-version: 2.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-17 14:53:53 -04:00
Sean Barbeau
8f07afeb83 chore: Bump version for release 2024-12-28 17:58:52 -05:00
Sean Barbeau
2196e66428 fix: Update to targetSdkVersion 34
Fixes https://github.com/barbeau/gpstest/issues/694
2024-12-28 17:57:28 -05:00
Sean Barbeau
8c050491a3 chore: Add version file for fastlane (F-Droid) 2024-12-28 17:23:04 -05:00
Sean Barbeau
b54c203e1b fix: Don't minify the library so we can build releases
See https://stackoverflow.com/questions/79102013/r8-removes-entire-classes-it-shouldnt
2024-12-28 17:14:25 -05:00
Sean Barbeau
b7b3fbc66e chore: Bump version for release 2024-12-28 16:50:42 -05:00
Sean Barbeau
bf0bb3a54d chore: Update What's new 2024-12-28 16:49:31 -05:00
Sean Barbeau
4292ea40da feat: Implementation runtime permissions for notification (#705)
Fixes https://github.com/barbeau/gpstest/issues/694 and fixes https://github.com/barbeau/gpstest/issues/622
2024-12-28 16:33:42 -05:00
barbeau
9e8f9734be fix: Try to fix GitHub CI 2024-12-10 13:51:11 -05:00
barbeau
a057fe448f fix: Fix Status log signal count and signal index values
Fixes https://github.com/barbeau/gpstest/issues/696
2024-12-10 13:44:51 -05:00
barbeau
aa8a6ff4fe chore: Update Gradle and wrapper versions 2024-12-10 13:35:59 -05:00
Slanterns
b069b82802 docs: Replace dead link in README.md (#693) 2024-10-15 12:13:05 -04:00
Corbin Davenport
1dab959ba4 docs: Replace deprecated goo.gl links (#687) 2024-07-19 16:03:56 -04:00
Sean Barbeau
8c258b0cca chore: Update stale.yml 2024-06-23 10:47:28 -04:00
Narugakuruga
221e46cb30 Add support for BDS B2b and IRNSS L1. And update signal detection to match the actual BDS (#657)
* Update full_description.txt

* Update CarrierFreqUtilsTest.kt

* Update CarrierFreqUtils.java
2023-10-27 11:28:48 -04:00
Sean Barbeau
58a643e1f0 docs: Remove donation links 2023-08-15 13:10:21 -04:00
Sean Barbeau
680b5fde9d chore: Bump version for release 2023-08-15 11:02:08 -04:00
barbeau
fd26076608 fix: Only inject assist data once per logging session
Fixes #648
2023-08-11 10:45:17 -04:00
barbeau
9afca7a975 chore: Set sensor update rate explicitly to 100Hz
This matches GnssLogger.

Related to #648
2023-08-09 09:40:22 -04:00
barbeau
822888dcfe fix: Write sensor data using Dispatchers.IO
Related to #648
2023-08-09 09:19:39 -04:00
barbeau
4c3d3865fe chore: Bump version for release 2023-04-27 15:15:08 -04:00
barbeau
3cea066d9c fix: Use LocationListenerCompat to avoid crashes on API <=30
Fixes #627
2023-04-27 15:14:10 -04:00
barbeau
74645d72dd chore: Remove unneeded Application references 2023-04-24 09:51:52 -04:00
barbeau
f7567f1a1d fix: Don't cap precision of entered benchmark coordinates
Fixes #628
2023-04-24 09:43:48 -04:00
barbeau
3dd022876b chore: Bump version for release 2023-04-21 13:06:11 -04:00
barbeau
e2825cab31 chore: Update What's New 2023-04-21 13:04:52 -04:00
barbeau
23f0de3d7b feat: Copy the log file to "Downloads/GPSTest" after logging (Android 11 and up)
Closes https://github.com/barbeau/gpstest/issues/633
2023-04-21 13:03:28 -04:00
barbeau
dea3e77225 fix: Add compatibility for older Android versions / lower end phones
* For API Level 29 and below, we need to implement all the methods of an interface.
* Handle the case where the device doesn't support the ORIENTATION sensor

Fixes https://github.com/barbeau/gpstest/issues/627
2023-04-20 17:34:34 -04:00
barbeau
3b17dff8b8 fix: Make binding nullable and handle cases where it could be null (i.e., callbacks)
Fixes https://github.com/barbeau/gpstest/issues/634
2023-04-20 16:41:21 -04:00
barbeau
b0e718670d fix: Remove decimal places from Status elevation and azimuth
Fixes https://github.com/barbeau/gpstest/issues/626
2023-04-20 16:10:09 -04:00
barbeau
592c69d18d fix: Check for geo URIs with empty lat or lon
Fixes https://github.com/barbeau/gpstest/issues/635
2023-04-20 16:05:23 -04:00
barbeau
bd4fead10f fix: Handle case where onCreateView() is called early for ShareDialogFragment
Fixes #636
2023-04-20 15:52:02 -04:00
barbeau
63c9ffdd3a chore: Fix formatting 2023-04-20 15:11:53 -04:00
Dave Collett
1e71bb9896 feat: Add SouthPAN support (#601)
Closes https://github.com/barbeau/gpstest/issues/271.
2023-04-20 15:06:32 -04:00
barbeau
2a64a40fd9 fix: Limit Google Maps KTX library to only google flavor (not osmdroid)
See https://github.com/barbeau/gpstest/pull/631
2023-04-19 09:34:54 -04:00
barbeau
c93d05b252 chore: Update Jackson dependency and proguard config 2023-04-12 16:28:49 -04:00
barbeau
271708ab2d fix: Don't indicate if time/data injection/deletion request succeeded
Fixes #607
2023-04-12 15:02:29 -04:00
barbeau
9ba377a60b chore: Add F-Droid release What's New 2023-04-12 14:57:05 -04:00
barbeau
51050de1f9 chore: Bump version for release 2023-04-12 14:54:05 -04:00
barbeau
613a2648e5 fix: Fix incorrect applicationId used when sharing files
Fixes #624
2023-04-12 14:52:37 -04:00
barbeau
95c6d99e21 docs: Update What's New 2023-04-12 14:28:38 -04:00
barbeau
d91808b081 fix: Add OSM attribution notices to F-Droid flavor maps
Fixes #620
2023-04-12 12:34:13 -04:00
barbeau
7f6d6305eb fix: Remove default sound and vibration for < API 26
Fixes #623
2023-04-12 11:57:15 -04:00
Sean Barbeau
a6b0acc432 chore: Target SDK version 31 (#621)
* Bump dependencies
 * COARSE location permissions
 * Pending intent mutability
 * Intent filters declaring exported
 * Scoped storage

Fixes #619, #447
2023-04-11 17:19:32 -04:00
barbeau
f4ce43ebb3 feat: Update launcher icons with Android Studio wizard, add monochrome version
Fixes #606
2023-04-11 17:17:48 -04:00
barbeau
779239182c fix: Enable background execution when file logging starts
Fixes https://github.com/barbeau/gpstest/issues/587
2023-04-04 14:51:58 -04:00
barbeau
08cb7b5cf8 fix: Update SBAS PRN assignments to June 2021 doc
See https://www.gps.gov/technical/prn-codes/L1-CA-PRN-code-assignments-2021-Jun.pdf

This seems to be the most recent reference from https://www.gps.gov/technical/prn-codes/.
2023-04-04 13:45:49 -04:00
barbeau
4b8b8a9e66 chore: Update app name, remove unused resources 2023-03-20 11:58:18 -04:00
barbeau
9f0b77ceb4 feat: Allow scrolling of Wear list using rotating bezel 2023-03-20 11:55:23 -04:00
barbeau
7995c01e50 chore: Set the top of the column closer to the top of the screen
This way the progress bar and "Lat" fields appear just under the clock, instead of leaving a large amount of space at the top of the screen (around half the screen).
2023-02-22 12:23:53 -05:00
barbeau
5130548ed8 chore: Wear - Replace hard-coded strings with translated strings from library 2023-02-22 12:19:25 -05:00
Ada
14ad3558af chore: Share the data formatting code between WearOS and phone (#616)
Closes #613
2023-02-22 12:11:46 -05:00
Ada
5d3fe854b7 fix: Initialize UI with empty values on Wear OS (#612) 2023-01-23 12:35:49 -05:00
Ada
7f56a0e849 feat: Add Wear OS Interface for GPSTest (#605)
* Add Wear OS Interface GPSTest

* Change the module and package name to wear

* Refactor packages to gpstest

* Change Time format

* Implement library in gpstest

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

* Solve the cross-module dependency injections problem about context

* Watch face with flashed problem

* Implement progress bar

* Add permissions request

* Resolve column overlap

* Add progress bar for Wear OS

* Update data from location on Wear OS

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

* Update timestamp from location.

* fix: OSMDroid build flavor

* Fix test bugs.

Co-authored-by: barbeau <barbeaus@google.com>
2023-01-10 09:55:22 -05:00
Sean Barbeau
b4f18fc345 fix: Location logging tests again 2022-07-18 18:09:09 -04:00
Sean Barbeau
c240fefcc4 fix: Location logging tests on older API levels 2022-07-18 14:32:59 -04:00
Sean Barbeau
f672d0e4c4 feat: Add isMock to location logging
Closes https://github.com/barbeau/gpstest/issues/589
2022-07-18 14:14:41 -04:00
Sean Barbeau
cd43cd688f fix: Check for fullBiasNanos before logging 2022-07-18 13:51:27 -04:00
Sean Barbeau
881dafc588 fix: GnssClock log formatting error
Closes https://github.com/barbeau/gpstest/issues/590
2022-07-18 13:48:07 -04:00
Sean Barbeau
d859f10fa0 fix: Ensure logged decimal values aren't in scientific notation
Closes https://github.com/barbeau/gpstest/issues/593
2022-07-18 13:40:44 -04:00
Klemen Skerbiš
1cefdf01df docs: Improved Readme - reorg, add screenshots (#572) 2021-12-14 14:27:15 -05:00
Sean Barbeau
6ba53f1948 docs: Update LICENSE 2021-12-09 12:41:44 -05:00
Sean Barbeau
2840a0f00b fix: Set the first fix flag with location in addition to GnssStatus
This should fix the "no fix" issue on devices that don't report "on first fix" via the GnssStatus API, because we'll use the Location API callback as well

Closes #495
2021-11-18 18:35:06 -05:00
Sean Barbeau
c1ffd27b53 feat: Enhance logging capabilities (#568)
Enhance logging in GPSTest

TODO:
- [x] Remove precision cap on decimal values (don't use `String.format()` - https://github.com/barbeau/gpstest/issues/553)
    - [x] Location
    - [x] Raw measurements
    - [x] NMEA
    - [x] Nav messages
    - [x] AntennaInfo
- [x] Add new GnssLogger info (described at https://www.kaggle.com/c/google-smartphone-decimeter-challenge/data# and https://github.com/barbeau/gpstest/issues/556)
    - [x] Location
    - [x] Raw measurements
    - [x] NMEA
    - [x] Nav messages
    - [x] GnssStatus
    - [x] Orientation
    - [x] GnssStatus - Settings UI
    - [x] Orientation - Settings UI
    - [x] Inject time on log - Settings UI
    - [x] Inject PSDS on log - Settings UI
- [x] Add location vertical accuracy (#557)
- [x] Archive logging README for v3 and older format
- [x] Update logging README with new v4 format
- [x] Update log header with new fields
    - [x] Location
    - [x] Raw measurements
    - [x] NMEA
    - [x] Nav messages
    - [x] GnssStatus
    - [x] Orientation

Closes https://github.com/barbeau/gpstest/issues/557
Closes https://github.com/barbeau/gpstest/issues/556
Closes https://github.com/barbeau/gpstest/issues/553
2021-11-18 18:30:01 -05:00
Sean Barbeau
ab4a4ac9ce fix: Only indicate fix is lost if gap is > 1.5 seconds
Closes #544
2021-11-15 18:14:49 -05:00
Sean Barbeau
a7ad3743a4 feat: Use viewModel for Sky fragment, including filter
Closes #563

Also bumps a number of libraries.

I hit a strange issue where the filter composable didn't seem to re-compose in Sky the same way it does in Status, presumably because it's mixed into an XML layout, when checking if the filter was active. It would only evaluate the IF statement once and then no longer recompose.

I got around this by putting a State observable first in the IF statement, which presumably avoids some type of optimization that's killing it the other way around:

 if (allStatuses.isNotEmpty() && gnssFilter().isNotEmpty()) {
2021-11-12 18:51:35 -05:00
Sean Barbeau
23b482d505 feat: Add background execution, convert Status to Composable (#550)
This is a major overhaul of the app internals to support a foreground service (i.e., a notification that shows current GNSS status even when the main Activity UI is not showing).

This involves moving all the API listeners for GNSS events into their own "manager" classes based on Kotlin callbackFlow that exist the entire lifecycle of the app, so the various app components (Fragments, ViewModels, Activity, Service) can all receive updates from this single source.

This bumps minSdkVersion to Android N (24) so we we don't need to deal with the legacy GPS* APIs anymore, which greatly simplifies the data processing within the app.

*  Convert Activity and Fragments to Kotlin
*  Add service with notification
*  Remove legacy GPS* APIs (pre-Android N)
*  Convert all location / GNSS API registrations to data managers based on Kotlin callbackFlow as detailed in https://barbeau.medium.com/kotlin-callbackflow-a-lightweight-architecture-for-location-aware-android-apps-f97b5e66aaa2.
    *  NMEA
    *  Location
    *  GNSS measurement
    *  Antenna
    *  Nav message
    *  Sensor (orientation)
* Add view binding to fragments
* Fix filter bug - it currently shows # signals being filtered, not number satellites (doesn't match # Sats field)
* Move aggregation of signals to satellites into Flow/ViewModel.
* Change Status to Composable - Use LiveData in ViewModel instead - see https://bladecoder.medium.com/kotlins-flow-in-viewmodels-it-s-complicated-556b472e281a that says don't use Flows in ViewModels.

Closes #299
Closes #492
2021-11-10 19:04:50 -05:00
Sean Barbeau
3fec6bf919 chore: Back off stalebot time 2021-10-31 11:25:31 -04:00
Sean Barbeau
2a6b14ca8e docs: Add F-Droid release notes info to README 2021-10-14 14:14:44 -04:00
Sean Barbeau
82801cc814 docs: Add F-Droid changelog for latest release
Closes #559
2021-10-14 14:11:52 -04:00
Sean Barbeau
69415661dd chore: Bump version for release 2021-10-04 11:55:49 -04:00
Sean Barbeau
baed7e3dd7 chore: Update translations 2021-10-04 10:14:42 -04:00
401 changed files with 14327 additions and 9892 deletions

14
.github/dependabot.yml vendored Normal file
View 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
View File

@@ -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:

View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

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

View File

@@ -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)
}
}
}

View File

@@ -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
)
)
}
}

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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)
}
}
}

View File

@@ -15,10 +15,10 @@
*/
package com.android.gpstest
import com.android.gpstest.model.GnssType
import com.android.gpstest.model.SatelliteStatus
import com.android.gpstest.model.SatelliteStatus.Companion.NO_DATA
import com.android.gpstest.model.SbasType
import com.android.gpstest.library.model.GnssType
import com.android.gpstest.library.model.SatelliteStatus
import com.android.gpstest.library.model.SatelliteStatus.Companion.NO_DATA
import com.android.gpstest.library.model.SbasType
/**
* Returns a status for a GPS NAVSTAR L1 signal
@@ -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
}

View File

@@ -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);
}

View File

@@ -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<>();
}
}

View 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
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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>

View File

@@ -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);
}
}

View 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
}
}

View File

@@ -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();
}
}

View File

@@ -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
}
}

View File

@@ -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);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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();
}

View File

@@ -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]);
}
}
}
}
}

View File

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

View File

@@ -1,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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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
*/

View File

@@ -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)

View File

@@ -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
*/

View File

@@ -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);
}
}

View File

@@ -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
*/

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View 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
}
}

View File

@@ -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));

View 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)
}
}

View File

@@ -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
)

View File

@@ -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)
}
}
}

View File

@@ -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)))
}
}
}

View File

@@ -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()
}
}

View File

@@ -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

View 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"
}
}

View File

@@ -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()
}
}
}

View File

@@ -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)
}
}

View File

@@ -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))
}

View 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()
}
}

View File

@@ -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);
}
});
}
}

View 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()
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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"

View File

@@ -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"/>

View File

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

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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>

View File

@@ -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"

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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>

View File

@@ -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"

View File

@@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -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>

View File

@@ -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<>();
}
}

View 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"
}
}

View File

@@ -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

View File

@@ -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);
}
/**

View File

@@ -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

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