mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 19:17:21 +00:00
Compare commits
456
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8138c948b | ||
|
|
0e28231c4a | ||
|
|
d6bdf5ecb6 | ||
|
|
6962a4bfa1 | ||
|
|
94a1a4f7b7 | ||
|
|
dbf2229774 | ||
|
|
f2fc1b7b28 | ||
|
|
d600836e19 | ||
|
|
94bffe80b0 | ||
|
|
4260fbb096 | ||
|
|
3d5b55faa3 | ||
|
|
466f2f6088 | ||
|
|
14bd984327 | ||
|
|
d5c100903b | ||
|
|
1c6388f3bc | ||
|
|
3e0874e6a8 | ||
|
|
6beaeb3e1a | ||
|
|
91f0cb1c56 | ||
|
|
3b72f8d68a | ||
|
|
3bc2983d26 | ||
|
|
0ec4c5dc6c | ||
|
|
8e8a3849df | ||
|
|
cd65e6c077 | ||
|
|
a3d370b1fa | ||
|
|
bf492e3f8b | ||
|
|
3591960d55 | ||
|
|
3f7a087938 | ||
|
|
4b82332133 | ||
|
|
dcd16a0196 | ||
|
|
9e88609efc | ||
|
|
322becdda8 | ||
|
|
7f93a10556 | ||
|
|
3cd5b49f98 | ||
|
|
4dbe896124 | ||
|
|
b47bd78ecd | ||
|
|
c89e79e28f | ||
|
|
b40a79e3c6 | ||
|
|
6c23696e22 | ||
|
|
f997d95cd4 | ||
|
|
e0a1d279ce | ||
|
|
059c816075 | ||
|
|
60dfa5d600 | ||
|
|
5f1eb239ed | ||
|
|
01ca06b43f | ||
|
|
22363c2549 | ||
|
|
45fe755470 | ||
|
|
0ce177cfbf | ||
|
|
f3fdf61784 | ||
|
|
fc028f852c | ||
|
|
7a865fffd7 | ||
|
|
89220a0b4b | ||
|
|
03d0055ce2 | ||
|
|
afb1e434e8 | ||
|
|
1bed7849a3 | ||
|
|
06f00e1601 | ||
|
|
462c3de155 | ||
|
|
4020381b03 | ||
|
|
b8389f63fe | ||
|
|
e64beee44f | ||
|
|
d2dffc039c | ||
|
|
d7d4a752ed | ||
|
|
6a3ab54226 | ||
|
|
3b3579696b | ||
|
|
826f44829b | ||
|
|
b979524b37 | ||
|
|
9e748f5534 | ||
|
|
25b982ac59 | ||
|
|
6a6399b606 | ||
|
|
c1cc992abd | ||
|
|
212dced3c4 | ||
|
|
7ef555e377 | ||
|
|
2773145e0c | ||
|
|
d8d3d81648 | ||
|
|
25a41def35 | ||
|
|
d9dc0a1735 | ||
|
|
692e917893 | ||
|
|
6371bb114d | ||
|
|
2d76aeda23 | ||
|
|
ecc24fd836 | ||
|
|
689f08881c | ||
|
|
d467200a45 | ||
|
|
6277e36a96 | ||
|
|
08a5c8c460 | ||
|
|
1ffb5ed9e8 | ||
|
|
cc684b6e3a | ||
|
|
6cb5b062d3 | ||
|
|
9c12b1eccb | ||
|
|
d56392f3c6 | ||
|
|
14b98cc7b1 | ||
|
|
200d2274de | ||
|
|
3d950f90ad | ||
|
|
1cc0bc770c | ||
|
|
84ba25c163 | ||
|
|
5bf4546ee3 | ||
|
|
030a6eecc9 | ||
|
|
42174da0be | ||
|
|
06421f8cfc | ||
|
|
9ea5b533c1 | ||
|
|
b6a9331ba3 | ||
|
|
390fd7717d | ||
|
|
ef0a34723b | ||
|
|
36d091a7e1 | ||
|
|
20143cf096 | ||
|
|
a64f9c80b9 | ||
|
|
db0320f521 | ||
|
|
adc9ac008b | ||
|
|
4a33d1b19a | ||
|
|
cce9209b3d | ||
|
|
237e4cb27a | ||
|
|
cd3eded77c | ||
|
|
7d7f8f1eda | ||
|
|
11b46c6710 | ||
|
|
da1facac64 | ||
|
|
cb1f341ed9 | ||
|
|
c2e496c406 | ||
|
|
7afce7ee68 | ||
|
|
86048ee1fd | ||
|
|
42a6ffa4c9 | ||
|
|
cf1d8e6539 | ||
|
|
ab8b072f1f | ||
|
|
1d4036e3d0 | ||
|
|
ab22244f86 | ||
|
|
3b0874d707 | ||
|
|
2c5b1479f1 | ||
|
|
39d4498a54 | ||
|
|
9ed2aebc9d | ||
|
|
72d5e13acf | ||
|
|
736dc23aae | ||
|
|
052ba79c94 | ||
|
|
cb648caf65 | ||
|
|
05170541ed | ||
|
|
e75d2dc410 | ||
|
|
d55a07bc26 | ||
|
|
c372386a77 | ||
|
|
de6ade025c | ||
|
|
2eb6d6cd5b | ||
|
|
c09f718aae | ||
|
|
a9fe0b74e9 | ||
|
|
2c9a5b376f | ||
|
|
422e480b2f | ||
|
|
efd6931abb | ||
|
|
c7ca77a943 | ||
|
|
be6473e692 | ||
|
|
75d162301b | ||
|
|
07c09ec3e9 | ||
|
|
513d50e56a | ||
|
|
366edd8bbb | ||
|
|
13df0dfcec | ||
|
|
eb1a5d51a2 | ||
|
|
4e3547649f | ||
|
|
7abbeb6eb3 | ||
|
|
5c73d5ed7a | ||
|
|
a663ef7f1b | ||
|
|
1f1e1fb144 | ||
|
|
5166daa297 | ||
|
|
604c2ec59d | ||
|
|
1904a6b3e6 | ||
|
|
7f390117b8 | ||
|
|
346803fe6e | ||
|
|
47b87a1604 | ||
|
|
935fe6d99f | ||
|
|
69af1c38bb | ||
|
|
5c35f2418d | ||
|
|
2aa9858660 | ||
|
|
bc6e065804 | ||
|
|
7924a6f69e | ||
|
|
9a2a715d1f | ||
|
|
ee32c7a1eb | ||
|
|
1d54ac4718 | ||
|
|
433ce85561 | ||
|
|
cebe3b7ec4 | ||
|
|
d8f60cf270 | ||
|
|
4a3332fdb5 | ||
|
|
9e7fdbfe71 | ||
|
|
463df58eb9 | ||
|
|
58b8801c0c | ||
|
|
2581ba92c0 | ||
|
|
e5131ebb72 | ||
|
|
f9accdd517 | ||
|
|
ee3c1e7058 | ||
|
|
a08a28df39 | ||
|
|
759a6de2c6 | ||
|
|
ba5a984c16 | ||
|
|
083b8a2847 | ||
|
|
e8ebfb1ef3 | ||
|
|
12eed1d9a9 | ||
|
|
b3859fbece | ||
|
|
f40a67e1b7 | ||
|
|
16dce5df2a | ||
|
|
80a58bed61 | ||
|
|
a8afc63bba | ||
|
|
dbf9a2a65f | ||
|
|
ddecaa6017 | ||
|
|
418a05e343 | ||
|
|
c80e351212 | ||
|
|
a333192df5 | ||
|
|
a7b41c95c7 | ||
|
|
2298d8eea4 | ||
|
|
1813e1bfa4 | ||
|
|
bd09ad22ff | ||
|
|
7e90071c4d | ||
|
|
d1cc944be2 | ||
|
|
fb28283679 | ||
|
|
81ae8772d2 | ||
|
|
571145838d | ||
|
|
017b178f2e | ||
|
|
670444858c | ||
|
|
7bdff5df81 | ||
|
|
df083840ac | ||
|
|
a0279bac17 | ||
|
|
3d630041e1 | ||
|
|
ea92d3c150 | ||
|
|
86ee3dcbac | ||
|
|
43788ba7a7 | ||
|
|
1a3f1bb34c | ||
|
|
998c98267b | ||
|
|
28fa373db0 | ||
|
|
4ca33c37ad | ||
|
|
bd42e643ea | ||
|
|
4baf3821e5 | ||
|
|
d7e1ce11c9 | ||
|
|
5de370983d | ||
|
|
f272e268ab | ||
|
|
e59507a4d3 | ||
|
|
0bae306312 | ||
|
|
65f61a5147 | ||
|
|
1064dc739e | ||
|
|
8aedd38b51 | ||
|
|
ad8f08a577 | ||
|
|
2afce54472 | ||
|
|
8e50fcf8fb | ||
|
|
e11b03d56b | ||
|
|
fa55b82a5a | ||
|
|
d57832b0d4 | ||
|
|
1d0e8921a9 | ||
|
|
23d95b8241 | ||
|
|
8af80866bc | ||
|
|
b9e5ff70f5 | ||
|
|
c176d6ad88 | ||
|
|
5a6b162e4f | ||
|
|
33712c29fc | ||
|
|
eac1e6e273 | ||
|
|
f585372594 | ||
|
|
4585332b1a | ||
|
|
6d2b0ced49 | ||
|
|
e86bc2c700 | ||
|
|
9a52fdfde5 | ||
|
|
07504a2a95 | ||
|
|
81525b6dd3 | ||
|
|
c7089ab314 | ||
|
|
77dfb7bb05 | ||
|
|
58fb688593 | ||
|
|
dc596b4348 | ||
|
|
f9b1a97a37 | ||
|
|
d700432829 | ||
|
|
b17ea2d918 | ||
|
|
976fdfd949 | ||
|
|
d8db258bd8 | ||
|
|
7b6fb5eb8f | ||
|
|
f4aef4f4f0 | ||
|
|
9147323db2 | ||
|
|
81d397a09d | ||
|
|
ec1a55c93d | ||
|
|
e5033cb2e9 | ||
|
|
39b786ae82 | ||
|
|
79809aaec0 | ||
|
|
f7f5a73c4c | ||
|
|
18c8ac1822 | ||
|
|
22607e7710 | ||
|
|
ad0ff6859e | ||
|
|
a39b1716e7 | ||
|
|
8a2339cfbf | ||
|
|
821fa0dfe7 | ||
|
|
ce8c2a4834 | ||
|
|
8cc4ef5610 | ||
|
|
dde22faf46 | ||
|
|
7ad81e32e1 | ||
|
|
5af963f3c9 | ||
|
|
bfb9fb6ca8 | ||
|
|
df24c66ef3 | ||
|
|
b166e896c2 | ||
|
|
18f8ab517d | ||
|
|
d3b8e951dd | ||
|
|
6e5a3cb0bb | ||
|
|
31d329a19b | ||
|
|
3df358b88c | ||
|
|
7d5bca300d | ||
|
|
fb2cd85b11 | ||
|
|
99158e74fa | ||
|
|
8265b73d75 | ||
|
|
d2b3184084 | ||
|
|
0c5d3699c0 | ||
|
|
a94f95874b | ||
|
|
4a3a01729b | ||
|
|
aadc285d7f | ||
|
|
2df0b9ba3d | ||
|
|
868155c532 | ||
|
|
3f981c09d5 | ||
|
|
b79fec13f4 | ||
|
|
6588291997 | ||
|
|
be96602c76 | ||
|
|
b59f9a6c17 | ||
|
|
9afe2ab69e | ||
|
|
2edc892dc2 | ||
|
|
45eb6915c2 | ||
|
|
706c912b13 | ||
|
|
e523eb5517 | ||
|
|
0bef2ffddc | ||
|
|
279594c425 | ||
|
|
8c1df334d8 | ||
|
|
e2bf2a09a4 | ||
|
|
1d48ce144f | ||
|
|
082b52c58b | ||
|
|
c1966442a7 | ||
|
|
4d88214c1d | ||
|
|
de99e09f5e | ||
|
|
e717d75324 | ||
|
|
a545863473 | ||
|
|
85ffe94266 | ||
|
|
7adbf951bc | ||
|
|
78dc217642 | ||
|
|
64137473b3 | ||
|
|
c08effb445 | ||
|
|
65410defe2 | ||
|
|
ed71c8148b | ||
|
|
13a79af27e | ||
|
|
f6564c3c71 | ||
|
|
8e42f81ea3 | ||
|
|
ada5b48ba5 | ||
|
|
a6a59877a8 | ||
|
|
6aa455490b | ||
|
|
f81738bc8b | ||
|
|
a57effd073 | ||
|
|
c180f6e58e | ||
|
|
1290b8f32f | ||
|
|
9e765a3575 | ||
|
|
65a0c33007 | ||
|
|
7add81e8b6 | ||
|
|
1a69822d3f | ||
|
|
c370d9febd | ||
|
|
24002827ed | ||
|
|
a835ac4e07 | ||
|
|
a68e466040 | ||
|
|
bfbe437ab1 | ||
|
|
574ffe09c2 | ||
|
|
529a68f291 | ||
|
|
266fbfed1f | ||
|
|
fb8053ef60 | ||
|
|
34e66f9ae0 | ||
|
|
10fc88364f | ||
|
|
2e5a15eec7 | ||
|
|
42aa4deb0d | ||
|
|
82f41940c7 | ||
|
|
6c3aeab041 | ||
|
|
609c83ed34 | ||
|
|
5a0ff019e9 | ||
|
|
ad89830c51 | ||
|
|
da2c3853b0 | ||
|
|
94735bc453 | ||
|
|
42e7fa4356 | ||
|
|
9ea4da9d21 | ||
|
|
a6cbd89ff1 | ||
|
|
aa9365b2fd | ||
|
|
bffb4842b1 | ||
|
|
fcec36ec68 | ||
|
|
483d846233 | ||
|
|
11841ca3d7 | ||
|
|
01139f76e7 | ||
|
|
fdbdc513f1 | ||
|
|
880fe10bfd | ||
|
|
5b62474c27 | ||
|
|
cdf642d9f3 | ||
|
|
e3955239f3 | ||
|
|
5842239e26 | ||
|
|
86512a44d2 | ||
|
|
d1b579ac4c | ||
|
|
5040767296 | ||
|
|
2b6a892a50 | ||
|
|
5aa837e5ef | ||
|
|
f1c978d167 | ||
|
|
34ec942e7b | ||
|
|
630fa2877f | ||
|
|
ad0b611bcb | ||
|
|
68d814d996 | ||
|
|
08767c5589 | ||
|
|
1f53ffbda9 | ||
|
|
8dc0cf7051 | ||
|
|
ae53e5dc35 | ||
|
|
213ac7c278 | ||
|
|
ab02abbf4e | ||
|
|
a1d5cda998 | ||
|
|
a6b5498f55 | ||
|
|
a52788e72f | ||
|
|
4b63882c70 | ||
|
|
549a915b24 | ||
|
|
19d0c1b108 | ||
|
|
269d48d250 | ||
|
|
a088a7a0a2 | ||
|
|
b9ade5f243 | ||
|
|
e100484810 | ||
|
|
b1aa623b29 | ||
|
|
e36e064374 | ||
|
|
d59f9c7506 | ||
|
|
49f713d82c | ||
|
|
1aefb04eb5 | ||
|
|
dca3f2c773 | ||
|
|
fc0b995bac | ||
|
|
5a8108384f | ||
|
|
7191e02f99 | ||
|
|
0ca372b73f | ||
|
|
c4ecdb7a78 | ||
|
|
1719f8cc2e | ||
|
|
3580854479 | ||
|
|
28e926eb86 | ||
|
|
5a449d4133 | ||
|
|
f8d5542af5 | ||
|
|
9bb73f28b7 | ||
|
|
7e2b3bc712 | ||
|
|
f8e7bcdf39 | ||
|
|
a807cfd8f6 | ||
|
|
f6fcd21404 | ||
|
|
208c587ddf | ||
|
|
5d2a870319 | ||
|
|
942c7ce65f | ||
|
|
4c13cc98a1 | ||
|
|
622d4e3632 | ||
|
|
542cfcfd50 | ||
|
|
12fabc8f23 | ||
|
|
f7f317cf8b | ||
|
|
6572dd4eab | ||
|
|
466c029be6 | ||
|
|
3da5b43e0a | ||
|
|
38ad7c46a8 | ||
|
|
05b5af41ba | ||
|
|
049d84e261 | ||
|
|
bf51fddc4d | ||
|
|
007e6d8f9d | ||
|
|
68d35f0104 | ||
|
|
39959c7a10 | ||
|
|
c592b8d3ce | ||
|
|
acb851d4f6 | ||
|
|
a08cfc7fb8 | ||
|
|
1ea4da3baf | ||
|
|
a817423e7e | ||
|
|
46f0b9608b | ||
|
|
ac479f80ee | ||
|
|
f13662747c | ||
|
|
f8d1909737 | ||
|
|
7628582fd7 | ||
|
|
b1ff0921c3 | ||
|
|
0755b20cf0 | ||
|
|
7dca788c01 | ||
|
|
853c36d19b | ||
|
|
ee5b5a894b | ||
|
|
8f411d4c5e | ||
|
|
0303c53a72 |
No files matched your search
@@ -0,0 +1,10 @@
|
||||
# indicate this is the root of the project
|
||||
root = true
|
||||
|
||||
[*.{kt,java,xml,gradle,md}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
end_of_line = lf
|
||||
@@ -0,0 +1,5 @@
|
||||
# GitHub Copilot Instructions
|
||||
|
||||
Read `AGENTS.md` first, then `CLAUDE.md`.
|
||||
|
||||
`AGENTS.md` contains the architecture, module boundaries, implementation details, conventions, and gotchas.
|
||||
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -1,70 +0,0 @@
|
||||
name: Look4Sat CI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Latest Commit
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup java environment
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Assemble APK and Bundle
|
||||
run: |
|
||||
chmod +x ./gradlew
|
||||
./gradlew assembleRelease
|
||||
./gradlew bundleRelease
|
||||
|
||||
- name: Sign APK
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
id: sign_apk
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/apk/release
|
||||
signingKeyBase64: ${{ secrets.KEY_STORE }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
alias: ${{ secrets.KEY_ALIAS }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: "35.0.1"
|
||||
|
||||
- name: Sign Bundle
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
id: sign_bundle
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/bundle/release
|
||||
signingKeyBase64: ${{ secrets.KEY_STORE }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
alias: ${{ secrets.KEY_ALIAS }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: "35.0.1"
|
||||
|
||||
- name: Rename APK and Bundle
|
||||
run: |
|
||||
mv ${{steps.sign_apk.outputs.signedReleaseFile}} app/build/outputs/apk/release/look4sat.apk
|
||||
mv ${{steps.sign_bundle.outputs.signedReleaseFile}} app/build/outputs/apk/release/look4sat.aab
|
||||
|
||||
- name: Deploy Bundle to Play Store
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{secrets.SERVICE_ACCOUNT_JSON}}
|
||||
packageName: com.rtbishop.look4sat
|
||||
releaseFiles: app/build/outputs/apk/release/look4sat.aab
|
||||
track: beta
|
||||
whatsNewDirectory: fastlane/metadata/android/en-US/whatsnew
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "app/build/outputs/apk/release/look4sat.apk,app/build/outputs/apk/release/look4sat.aab"
|
||||
bodyFile: fastlane/metadata/android/en-US/whatsnew/whatsnew-en-US
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
@@ -0,0 +1,81 @@
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
|
||||
- name: Assemble Artifacts
|
||||
run: ./gradlew assembleRelease bundleRelease
|
||||
|
||||
- name: Sign APK
|
||||
run: |
|
||||
echo "${{ secrets.KEY_STORE }}" | base64 -d > keystore.jks
|
||||
APK=$(find app/build/outputs/apk/release -name "*.apk" | head -1)
|
||||
BUILD_TOOLS=$(ls -d ${ANDROID_HOME}/build-tools/*/ | sort -V | tail -1)
|
||||
${BUILD_TOOLS}apksigner sign \
|
||||
--ks keystore.jks \
|
||||
--ks-pass pass:${{ secrets.KEY_STORE_PASSWORD }} \
|
||||
--ks-key-alias ${{ secrets.KEY_ALIAS }} \
|
||||
--key-pass pass:${{ secrets.KEY_PASSWORD }} \
|
||||
--out app/build/outputs/apk/release/look4sat.apk \
|
||||
"$APK"
|
||||
rm keystore.jks
|
||||
|
||||
- name: Sign Bundle
|
||||
run: |
|
||||
echo "${{ secrets.KEY_STORE }}" | base64 -d > keystore.jks
|
||||
AAB=$(find app/build/outputs/bundle/release -name "*.aab" | head -1)
|
||||
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \
|
||||
-keystore keystore.jks \
|
||||
-storepass ${{ secrets.KEY_STORE_PASSWORD }} \
|
||||
-keypass ${{ secrets.KEY_PASSWORD }} \
|
||||
"$AAB" ${{ secrets.KEY_ALIAS }}
|
||||
rm keystore.jks
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.4'
|
||||
|
||||
- name: Deploy Bundle to Google Play
|
||||
run: |
|
||||
gem install multi_json
|
||||
gem install fastlane --no-document
|
||||
AAB=$(find app/build/outputs/bundle/release -name "*.aab" | head -1)
|
||||
echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
||||
fastlane supply \
|
||||
--aab "$AAB" \
|
||||
--json_key service_account.json \
|
||||
--package_name com.rtbishop.look4sat \
|
||||
--track production \
|
||||
--skip_upload_images true \
|
||||
--skip_upload_screenshots true \
|
||||
rm service_account.json
|
||||
|
||||
- name: Create Release
|
||||
run: |
|
||||
gh release create $TAG_NAME --title=$TAG_NAME --generate-notes
|
||||
gh release upload $TAG_NAME app/build/outputs/apk/release/look4sat.apk
|
||||
+7
-1
@@ -1,6 +1,9 @@
|
||||
# Built application files
|
||||
*.ap_
|
||||
|
||||
# Hermes AI plans and metadata
|
||||
.hermes/
|
||||
|
||||
# Files for the ART/Dalvik VM
|
||||
*.dex
|
||||
|
||||
@@ -15,6 +18,7 @@ out/
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
.cxx/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
@@ -62,4 +66,6 @@ fastlane/readme.md
|
||||
/app/release/app-release.aab
|
||||
/app/release/app-release.apk
|
||||
/app/release/output-metadata.json
|
||||
/app/release/
|
||||
/app/release/
|
||||
/.kotlin/sessions/
|
||||
dist/
|
||||
@@ -0,0 +1,122 @@
|
||||
# Look4Sat AI Agent Instructions
|
||||
|
||||
This is the canonical project guide for all AI assistants working on Look4Sat.
|
||||
All assistant-specific files (`CLAUDE.md`, `.github/copilot-instructions.md`) point here.
|
||||
|
||||
---
|
||||
|
||||
## Project Overview
|
||||
|
||||
Look4Sat is an open-source, fully offline Android satellite tracker and pass predictor. It tracks 9000+ active
|
||||
satellites using TLE/OMM data from Celestrak/SatNOGS, calculates orbital positions via SGP4/SDP4 models, and displays
|
||||
passes relative to the user's location. Features include polar radar visualization, SSTV image decoding, satellite
|
||||
ground track mapping, and pass predictions up to 10 days ahead. No ads, no tracking, no network required after initial
|
||||
data download.
|
||||
|
||||
## Architecture
|
||||
|
||||
**MVI (Model-View-Intent)** with unidirectional data flow:
|
||||
- `State` data class → exposed via `StateFlow` from ViewModel
|
||||
- `Action` sealed interface → user intents dispatched to ViewModel's `onAction()`
|
||||
- Jetpack Compose UI observes state and recomposes reactively
|
||||
|
||||
**Clean Architecture layers:**
|
||||
|
||||
| Module | Responsibility |
|
||||
|----------------------|---------------------------------------------------------------------|
|
||||
| `app` | Entry point. Aggregates all modules |
|
||||
| `core:data` | Android library. Room DB, OkHttp networking, repo implementations |
|
||||
| `core:domain` | Pure Kotlin (JVM). Orbital math (SGP4/SDP4), models, repo contracts |
|
||||
| `core:presentation` | Android library. Compose theme, shared UI components, NavKeys |
|
||||
| `feature:map` | OSMDroid map with ground tracks |
|
||||
| `feature:passes` | Pass predictions and upcoming events |
|
||||
| `feature:radar` | Polar radar view of satellite positions, SSTV image decoding |
|
||||
| `feature:satellites` | Satellite list, filtering, selection |
|
||||
| `feature:settings` | User preferences |
|
||||
|
||||
- `feature:*` modules depend only on `core:domain` + `core:presentation`. Features never depend on each other.
|
||||
|
||||
## Build & Run
|
||||
|
||||
```shell
|
||||
# Debug build
|
||||
./gradlew assembleDebug
|
||||
|
||||
# Release build (minified, shrunk resources)
|
||||
./gradlew assembleRelease
|
||||
|
||||
# Run tests
|
||||
./gradlew test
|
||||
```
|
||||
|
||||
- **Min SDK**: 24 | **Target SDK**: 36 | **JDK**: 17
|
||||
- **Gradle**: Uses version catalog (`gradle/libs.versions.toml`) + convention plugins in `build-logic/`
|
||||
|
||||
## Key Libraries
|
||||
|
||||
- **Compose** (BOM 2026.05.01) + Material3 Adaptive
|
||||
- **Navigation3** (type-safe, uses `@Serializable` NavKeys)
|
||||
- **Room** (KSP code generation) for local satellite/TLE storage
|
||||
- **OkHttp** 5.x for TLE downloads
|
||||
- **OSMDroid** for map rendering
|
||||
- **Kotlin Serialization** for navigation args and data parsing
|
||||
- **Coroutines** + `StateFlow` for async/reactive patterns
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Minimal dependencies**: Avoid adding libraries when a simple manual solution exists. Fewer deps = less maintenance.
|
||||
- **DI**: Manual — ViewModels use companion `factory()` methods with `IMainContainer` interface.
|
||||
- **Navigation**: Type-safe Compose Navigation3 with `@Serializable` data classes as nav keys.
|
||||
- **State naming**: `<Feature>State` data class + `<Feature>Action` sealed interface per feature.
|
||||
- **No feature-to-feature deps**: All cross-feature communication goes through core layers.
|
||||
- **Localization**: 7 languages (en, es, ru, si, tr, uk, zh).
|
||||
|
||||
## Data Formats & Migration
|
||||
|
||||
**TLE vs. OMM/CSV format:**
|
||||
|
||||
Look4Sat supports both TLE and OMM (Orbit Mean-Elements Message) formats for backward compatibility:
|
||||
|
||||
- **TLE format**: Traditional 3-line element format (deprecated). NORAD catalog numbers are 5-digit integers, which
|
||||
are running out of space. Celestrak has signaled that TLE format will eventually be phased out.
|
||||
- **OMM/CSV format**: The future standard. CSV files contain the same orbital parameters as TLE but use ISO 8601
|
||||
timestamps and support larger NORAD IDs. Celestrak and SatNOGS already provide OMM data in CSV format.
|
||||
|
||||
**Current implementation:**
|
||||
- `DataParser.kt` handles both `parseTLEStream()` and `parseCSVStream()` seamlessly
|
||||
- TLE data is downloaded from configured sources and stored in Room database
|
||||
- When downloading satellite data, the app automatically detects format and parses accordingly
|
||||
- Both formats produce identical `OrbitalData` objects, ensuring transparent format switching
|
||||
|
||||
**Migration path:**
|
||||
As NORAD catalog space becomes constrained, OMM/CSV will become the primary format. Look4Sat is already positioned
|
||||
to handle this transition without code changes — existing users can continue using TLE files while new sources
|
||||
transition to OMM/CSV automatically.
|
||||
|
||||
## Code Style
|
||||
|
||||
- Prefer **short, focused functions** — single responsibility, easy to read.
|
||||
- **Exceptions**: Composable functions and math-heavy algorithms (SGP4/SDP4) may be longer.
|
||||
- Strict code style — no dead code, no unused imports, consistent formatting.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- **KMP migration**: `core:domain` is to become a fully shareable KMM module. Keep it pure Kotlin/JVM.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Orbital math lives in `core:domain/predict/` — it's dense vector math (SGP4/SDP4). Tread carefully.
|
||||
- TLE/OMM data must be refreshed weekly for accurate predictions (satellite orbits decay). TLE format is legacy and
|
||||
will eventually be deprecated in favor of OMM/CSV as NORAD catalog numbers approach the 5-digit limit.
|
||||
- SSTV decoding in `feature:radar` is experimental; image quality depends on signal strength during satellite pass.
|
||||
- `build-logic/convention/` contains all shared Gradle configuration — edit there, not in individual modules.
|
||||
- ProGuard is enabled for release builds — don't add reflection-based libs or any other dependencies without asking.
|
||||
|
||||
## Copilot Working Mode: Code-Only
|
||||
|
||||
- Default to code changes only. Provide explanations in chat only.
|
||||
- If documentation seems useful, ask first before creating files.
|
||||
- Do NOT create any `.md` documentation files unless explicitly requested.
|
||||
- Do NOT add README, guides, summaries, migration notes, or how-to files unless asked.
|
||||
- Prefer minimal diffs focused on requested implementation.
|
||||
- Default validation is static checks (`get_errors`). Do NOT run Gradle compile/test tasks unless explicitly requested.
|
||||
@@ -0,0 +1,5 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Read `AGENTS.md` first, then follow the instructions there.
|
||||
|
||||
`AGENTS.md` contains the architecture, module boundaries, implementation details, conventions, and gotchas.
|
||||
@@ -1,34 +1,42 @@
|
||||
# Look4Sat: Satellite tracker
|
||||
# Look4Sat-BA7OPF
|
||||
|
||||
[](https://github.com/rt-bishop/Look4Sat/actions/workflows/main.yml)
|
||||
[](https://github.com/atsunatsu/Look4Sat/releases)
|
||||
|
||||
[<img src="https://play.google.com/intl/en_gb/badges/static/images/badges/en_badge_web_generic.png" alt="Get it on Google Play" height="80">](https://play.google.com/store/apps/details?id=com.rtbishop.look4sat)
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80">](https://f-droid.org/packages/com.rtbishop.look4sat/)
|
||||
**BA7OPF 定制版** — 基于 [rt-bishop/Look4Sat](https://github.com/rt-bishop/Look4Sat) 的业余无线电卫星追踪器,增加了线性卫星频率计算器等功能。
|
||||
|
||||
### Radio satellite tracker and pass predictor for Android, inspired by Gpredict
|
||||
## 本仓库特色功能
|
||||
|
||||
<p float="left">
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" width="180"/>
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/2.png" width="180"/>
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/3.png" width="180"/>
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4.png" width="180">
|
||||
</p>
|
||||
- **网格地图模式(Grid Mode)** — 地图页内置 VUCC 网格覆盖图:已通联网格绿色填充、漫游网格蓝色 45° 斜纹(疏朗半透明样式)、当前所在方格淡黄加粗框;点击已通联网格弹出该网格内通联明细弹窗;进入网格模式默认 VUCC 视图
|
||||
- **奖状边界模式(Award Boundaries)** — 在网格地图上叠加 DXCC / WAPC / WAJA / WAZ / WAS 国界·省界·分区边界,已确认区域绿色填充
|
||||
- **LoTW 网格同步** — 直连 ARRL Logbook(账号密码验证)下载确认报告,worked / roamed 网格自动更新(兼容任意字段顺序的 ADIF 解析);同步失败按凭据错误/限流/超时分类提示
|
||||
- **双站过境匹配(Mutual Pass)** — 输入友台经纬度/网格,筛选双方同时可见的卫星过境,在地图上同时显示双方仰角曲线和地面轨迹,支持一键跳转雷达页查看详情
|
||||
- **线性卫星转发器频率计算器** — 在雷达页的 Calculator 标签页中,支持 TX/RX 双向多普勒频率计算,以及下行频率偏移(offset)输入,方便操作带偏移的线性卫星
|
||||
- **CW 解码器** — 集成 Morse Expert 解码引擎,支持瀑布图、实时解码文本(arm64 + armeabi-v7a 双架构)
|
||||
- **AMSAT 状态页** — 实时查看卫星的业余无线电转发器状态(开启/关闭/待机),支持 72 小时历史记录回放,启动时预取缓存
|
||||
- **应用内更新检查** — 设置页一键检查 GitHub 最新 Release,展示版本说明并直接下载安装
|
||||
- **自定义 TLE 数据源** — 可添加自定义 TLE/Celestrak CSV 数据源,长按拖拽排序,数据源启停开关与 HTTP 状态码显示
|
||||
- **中文界面优化** — 翻译修正、UI 布局调整,系统语言自动切换
|
||||
|
||||
### Track satellite passes with ease!
|
||||
## 上游仓库
|
||||
|
||||
Thanks to the huge database provided by [Celestrak](https://celestrak.com/) and [SatNOGS](https://satnogs.org/) you have access to more than 5000 active satellites orbiting Earth. You can search the entire DB by satellite name or by NORAD catnum.
|
||||
本仓库是 [rt-bishop/Look4Sat](https://github.com/rt-bishop/Look4Sat) 的分支,上游仓库的原始功能包括:
|
||||
|
||||
Satellite positions and passes are calculated relative to your location. To get reliable info make sure to set the observation position using GPS or QTH Locator in the Settings menu.
|
||||
- 基于 Celestrak / SatNOGS 数据的 9000+ 活跃卫星追踪
|
||||
- SGP4/SDP4 轨道预测,10 天过境预报
|
||||
- 极坐标雷达图、地面轨迹图
|
||||
- SSTV 图像解码
|
||||
- 无广告、无跟踪、完全离线
|
||||
|
||||
The application is built using Kotlin, Coroutines, Architecture Components and Jetpack Navigation. It is now and always will be completely ad-free and open-source.
|
||||
## 许可证
|
||||
|
||||
Huge thanks to [DownloadAstro](https://appoftheday.downloadastro.com/app/look4sat-satellite-tracker/) team for their interest to the app and the interview published.
|
||||
GNU General Public License v3.0。详见 [LICENSE](LICENSE)。
|
||||
|
||||
## Main features:
|
||||
## Star History
|
||||
|
||||
* Predicting satellite positions and passes for up to a week
|
||||
* Showing the list of currently active and upcoming satellite passes
|
||||
* Showing the active pass progress, polar trajectory and transceivers info
|
||||
* Showing the satellite positional data, footprint and ground track on a map
|
||||
* Custom TLE data import is available via files with TXT or TLE extensions
|
||||
* Offline first: calculations are made offline. Weekly update of TLE data is recommended.
|
||||
<a href="https://star-history.dera.page/#atsunatsu/Look4Sat&type=timeline&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=atsunatsu/Look4Sat&type=timeline&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=atsunatsu/Look4Sat&type=timeline&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=atsunatsu/Look4Sat&type=timeline&legend=top-left" />
|
||||
</picture>
|
||||
</a>
|
||||
@@ -1,114 +0,0 @@
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "org.jetbrains.kotlin.android"
|
||||
id "org.jetbrains.kotlin.kapt"
|
||||
id "dagger.hilt.android.plugin"
|
||||
id "androidx.navigation.safeargs.kotlin"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.rtbishop.look4sat'
|
||||
compileSdk 35
|
||||
defaultConfig {
|
||||
applicationId "com.rtbishop.look4sat"
|
||||
minSdk 21
|
||||
targetSdk 35
|
||||
versionCode 323
|
||||
versionName '3.2.3'
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
kapt {
|
||||
arguments {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
arg("room.incremental", "true")
|
||||
arg("room.expandProjection", "true")
|
||||
}
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
// compose true
|
||||
viewBinding true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion compose_compiler_version
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
testOptions {
|
||||
animationsDisabled true
|
||||
unitTests.includeAndroidResources true
|
||||
}
|
||||
androidResources {
|
||||
generateLocaleConfig true
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(":base")
|
||||
implementation "androidx.core:core-ktx:$core_ktx_version"
|
||||
implementation "androidx.core:core-splashscreen:$core_splashscreen_version"
|
||||
implementation "androidx.constraintlayout:constraintlayout:$constraint_version"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
||||
implementation "androidx.navigation:navigation-runtime-ktx:$navigation_version"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
implementation "androidx.room:room-ktx:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
implementation "com.google.android.material:material:$material_version"
|
||||
implementation "com.google.dagger:hilt-android:$hilt_version"
|
||||
kapt "com.google.dagger:hilt-compiler:$hilt_version"
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||
implementation "org.osmdroid:osmdroid-android:$osmdroid_version"
|
||||
|
||||
// implementation "androidx.compose.ui:ui:$compose_version"
|
||||
// implementation "androidx.compose.ui:ui-tooling:$compose_version"
|
||||
// implementation "androidx.compose.runtime:runtime:$compose_version"
|
||||
// implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
|
||||
// implementation "androidx.compose.compiler:compiler:$compose_compiler_version"
|
||||
// implementation "androidx.compose.animation:animation:$compose_version"
|
||||
// implementation "androidx.compose.foundation:foundation:$compose_foundation_version"
|
||||
// implementation "androidx.activity:activity-compose:$activity_compose_version"
|
||||
// implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
|
||||
// implementation "androidx.compose.material:material:$compose_material_version"
|
||||
// implementation "androidx.compose.material:material-icons-extended:$compose_material_version"
|
||||
// implementation "com.google.android.material:compose-theme-adapter:$material_adapter_version"
|
||||
|
||||
// debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
|
||||
debugImplementation "androidx.fragment:fragment-testing:$fragment_test_version"
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakcanary_version"
|
||||
|
||||
testImplementation "junit:junit:$junit_version"
|
||||
testImplementation "org.mockito:mockito-core:$mockito_version"
|
||||
testImplementation "org.robolectric:robolectric:$robolectric_version"
|
||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_test_version"
|
||||
|
||||
androidTestImplementation "androidx.test:core:$androidx_test_version"
|
||||
androidTestImplementation "androidx.test:runner:$androidx_test_version"
|
||||
androidTestImplementation "androidx.test:rules:$androidx_test_version"
|
||||
androidTestImplementation "androidx.test.ext:junit:$androidx_junit_version"
|
||||
androidTestImplementation "androidx.test.ext:truth:$androidx_test_version"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
|
||||
// androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
|
||||
androidTestImplementation "org.mockito:mockito-android:$mockito_version"
|
||||
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_test_version"
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import java.io.FileInputStream
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.convention.applicationPlugin)
|
||||
}
|
||||
|
||||
// Release signing credentials live in local.properties (gitignored) or
|
||||
// environment variables — never hardcode passwords in VCS.
|
||||
val releaseProps = Properties().apply {
|
||||
val propsFile = rootProject.file("local.properties")
|
||||
if (propsFile.exists()) FileInputStream(propsFile).use { load(it) }
|
||||
}
|
||||
fun releaseCred(name: String): String =
|
||||
releaseProps.getProperty(name) ?: System.getenv(name) ?: ""
|
||||
|
||||
android {
|
||||
namespace = libs.versions.packageName.get()
|
||||
defaultConfig {
|
||||
applicationId = "cn.ba7opf.look4sat"
|
||||
// CW decoder ships for both ABIs (see feature/cw jniLibs); the APK must
|
||||
// not be pinned to 32-bit only or 64-bit devices fall back to ARM32.
|
||||
ndk {
|
||||
abiFilters.add("armeabi-v7a")
|
||||
abiFilters.add("arm64-v8a")
|
||||
}
|
||||
}
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
storeFile = file(System.getProperty("user.home") + "/my-release-key.jks")
|
||||
storePassword = releaseCred("RELEASE_STORE_PASSWORD")
|
||||
keyAlias = releaseCred("RELEASE_KEY_ALIAS").ifEmpty { "look4sat" }
|
||||
keyPassword = releaseCred("RELEASE_KEY_PASSWORD")
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
-11
@@ -1,11 +0,0 @@
|
||||
# Please add these rules to your existing keep rules in order to suppress warnings.
|
||||
# This is generated automatically by the Android Gradle plugin.
|
||||
-dontwarn org.bouncycastle.jsse.BCSSLParameters
|
||||
-dontwarn org.bouncycastle.jsse.BCSSLSocket
|
||||
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
|
||||
-dontwarn org.conscrypt.Conscrypt$Version
|
||||
-dontwarn org.conscrypt.Conscrypt
|
||||
-dontwarn org.conscrypt.ConscryptHostnameVerifier
|
||||
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
|
||||
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
||||
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
||||
@@ -1,198 +0,0 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "834981e960b712254e56b633f3772115",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "entries",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`comment` TEXT, `name` TEXT NOT NULL, `epoch` REAL NOT NULL, `meanmo` REAL NOT NULL, `eccn` REAL NOT NULL, `incl` REAL NOT NULL, `raan` REAL NOT NULL, `argper` REAL NOT NULL, `meanan` REAL NOT NULL, `catnum` INTEGER NOT NULL, `bstar` REAL NOT NULL, `xincl` REAL NOT NULL, `xnodeo` REAL NOT NULL, `omegao` REAL NOT NULL, `xmo` REAL NOT NULL, `xno` REAL NOT NULL, `isDeepspace` INTEGER NOT NULL, PRIMARY KEY(`catnum`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "comment",
|
||||
"columnName": "comment",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.epoch",
|
||||
"columnName": "epoch",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.meanmo",
|
||||
"columnName": "meanmo",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.eccn",
|
||||
"columnName": "eccn",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.incl",
|
||||
"columnName": "incl",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.raan",
|
||||
"columnName": "raan",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.argper",
|
||||
"columnName": "argper",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.meanan",
|
||||
"columnName": "meanan",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.catnum",
|
||||
"columnName": "catnum",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.bstar",
|
||||
"columnName": "bstar",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.xincl",
|
||||
"columnName": "xincl",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.xnodeo",
|
||||
"columnName": "xnodeo",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.omegao",
|
||||
"columnName": "omegao",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.xmo",
|
||||
"columnName": "xmo",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.xno",
|
||||
"columnName": "xno",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.isDeepspace",
|
||||
"columnName": "isDeepspace",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"catnum"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "radios",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `info` TEXT NOT NULL, `isAlive` INTEGER NOT NULL, `downlink` INTEGER, `uplink` INTEGER, `mode` TEXT, `isInverted` INTEGER NOT NULL, `catnum` INTEGER, `comment` TEXT, PRIMARY KEY(`uuid`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "uuid",
|
||||
"columnName": "uuid",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "info",
|
||||
"columnName": "info",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlive",
|
||||
"columnName": "isAlive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "downlink",
|
||||
"columnName": "downlink",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uplink",
|
||||
"columnName": "uplink",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mode",
|
||||
"columnName": "mode",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isInverted",
|
||||
"columnName": "isInverted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "catnum",
|
||||
"columnName": "catnum",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "comment",
|
||||
"columnName": "comment",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"uuid"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '834981e960b712254e56b633f3772115')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 2,
|
||||
"identityHash": "f43ab966fb7b0508057c359530582ff5",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "entries",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`comment` TEXT, `name` TEXT NOT NULL, `epoch` REAL NOT NULL, `meanmo` REAL NOT NULL, `eccn` REAL NOT NULL, `incl` REAL NOT NULL, `raan` REAL NOT NULL, `argper` REAL NOT NULL, `meanan` REAL NOT NULL, `catnum` INTEGER NOT NULL, `bstar` REAL NOT NULL, `xincl` REAL NOT NULL, `xnodeo` REAL NOT NULL, `omegao` REAL NOT NULL, `xmo` REAL NOT NULL, `xno` REAL NOT NULL, `orbitalPeriod` REAL NOT NULL, `isDeepSpace` INTEGER NOT NULL, PRIMARY KEY(`catnum`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "comment",
|
||||
"columnName": "comment",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.epoch",
|
||||
"columnName": "epoch",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.meanmo",
|
||||
"columnName": "meanmo",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.eccn",
|
||||
"columnName": "eccn",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.incl",
|
||||
"columnName": "incl",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.raan",
|
||||
"columnName": "raan",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.argper",
|
||||
"columnName": "argper",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.meanan",
|
||||
"columnName": "meanan",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.catnum",
|
||||
"columnName": "catnum",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.bstar",
|
||||
"columnName": "bstar",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.xincl",
|
||||
"columnName": "xincl",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.xnodeo",
|
||||
"columnName": "xnodeo",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.omegao",
|
||||
"columnName": "omegao",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.xmo",
|
||||
"columnName": "xmo",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.xno",
|
||||
"columnName": "xno",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.orbitalPeriod",
|
||||
"columnName": "orbitalPeriod",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data.isDeepSpace",
|
||||
"columnName": "isDeepSpace",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"catnum"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "radios",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `info` TEXT NOT NULL, `isAlive` INTEGER NOT NULL, `downlink` INTEGER, `uplink` INTEGER, `mode` TEXT, `isInverted` INTEGER NOT NULL, `catnum` INTEGER, `comment` TEXT, PRIMARY KEY(`uuid`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "uuid",
|
||||
"columnName": "uuid",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "info",
|
||||
"columnName": "info",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlive",
|
||||
"columnName": "isAlive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "downlink",
|
||||
"columnName": "downlink",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uplink",
|
||||
"columnName": "uplink",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mode",
|
||||
"columnName": "mode",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isInverted",
|
||||
"columnName": "isInverted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "catnum",
|
||||
"columnName": "catnum",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "comment",
|
||||
"columnName": "comment",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"uuid"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f43ab966fb7b0508057c359530582ff5')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.rtbishop.look4sat
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.rtbishop.bbctestapp", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.BLUETOOTH"
|
||||
android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:name=".presentation.MainApplication"
|
||||
android:name=".MainApplication"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Look4Sat.Main">
|
||||
android:roundIcon="@mipmap/ic_launcher_round">
|
||||
|
||||
<activity
|
||||
android:name=".presentation.MainActivity"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Look4Sat.SplashScreen">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<!-- <intent-filter android:autoVerify="true">-->
|
||||
<!-- <action android:name="android.intent.action.VIEW" />-->
|
||||
<!-- <category android:name="android.intent.category.DEFAULT" />-->
|
||||
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
|
||||
<!-- <data android:scheme="https" />-->
|
||||
<!-- <data android:host="github.com" />-->
|
||||
<!-- <data android:pathPattern="/rt-bishop/Look4Sat/passes.*" />-->
|
||||
<!-- </intent-filter>-->
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
<meta-data
|
||||
android:name="android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED"
|
||||
android:value="com.rtbishop.look4sat" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.ColorMatrix
|
||||
import android.graphics.ColorMatrixColorFilter
|
||||
import android.graphics.Paint
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
||||
import com.rtbishop.look4sat.core.presentation.MainTheme
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
val config = Configuration(newBase?.resources?.configuration)
|
||||
applyOverrideConfiguration(config.apply { fontScale = 1.0f })
|
||||
super.attachBaseContext(newBase)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
observeNightFilterState()
|
||||
setContent {
|
||||
MainTheme(isDarkTheme = true) { NavRoot() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeNightFilterState() {
|
||||
val mainContainer = (applicationContext as IContainerProvider).getMainContainer()
|
||||
lifecycleScope.launch {
|
||||
mainContainer.settingsRepo.otherSettings
|
||||
.map { it.stateOfNightMode }
|
||||
.distinctUntilChanged()
|
||||
.collect { nightMode -> applyNightFilter(nightMode) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyNightFilter(enabled: Boolean) {
|
||||
if (enabled) {
|
||||
val nightMatrix = ColorMatrix(
|
||||
floatArrayOf(
|
||||
1f, 0f, 0f, 0f, 0f, // R → R
|
||||
0f, 0f, 0f, 0f, 0f, // G → 0
|
||||
0f, 0f, 0f, 0f, 0f, // B → 0
|
||||
0f, 0f, 0f, 1f, 0f // A → A
|
||||
)
|
||||
)
|
||||
window.decorView.setLayerType(View.LAYER_TYPE_HARDWARE, Paint().apply {
|
||||
colorFilter = ColorMatrixColorFilter(nightMatrix)
|
||||
})
|
||||
} else {
|
||||
window.decorView.setLayerType(View.LAYER_TYPE_NONE, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.app.Application.ActivityLifecycleCallbacks
|
||||
import android.os.Bundle
|
||||
import com.rtbishop.look4sat.core.data.injection.MainContainer
|
||||
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
||||
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWSyncMode
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWResult
|
||||
import com.rtbishop.look4sat.core.domain.repository.applyLoTWGridResult
|
||||
import com.rtbishop.look4sat.core.domain.repository.lotwCursorApi
|
||||
import com.rtbishop.look4sat.core.domain.repository.lotwSyncToday
|
||||
import com.rtbishop.look4sat.core.domain.repository.resolveLoTWSyncMode
|
||||
import com.rtbishop.look4sat.core.domain.repository.shouldAutoSyncLoTW
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class MainApplication : Application(), IContainerProvider {
|
||||
|
||||
private lateinit var container: IMainContainer
|
||||
|
||||
override fun getMainContainer(): IMainContainer = container
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
container = MainContainer(this)
|
||||
clearAmSatCacheOnAppBackground()
|
||||
// trigger automatic update every 48 hours
|
||||
container.appScope.launch { checkAutoUpdate() }
|
||||
// automatic LoTW grid sync on every app start (gated, see checkLoTWAutoSync)
|
||||
container.appScope.launch { checkLoTWAutoSync() }
|
||||
// load satellite data on every app start
|
||||
container.appScope.launch { container.satelliteRepo.initRepository() }
|
||||
}
|
||||
|
||||
private fun clearAmSatCacheOnAppBackground() {
|
||||
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
|
||||
private var startedActivityCount = 0
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {
|
||||
if (startedActivityCount == 0) {
|
||||
container.appScope.launch { container.amSatRepo.prefetchStatus() }
|
||||
}
|
||||
startedActivityCount += 1
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {
|
||||
startedActivityCount = (startedActivityCount - 1).coerceAtLeast(0)
|
||||
if (startedActivityCount == 0) container.amSatRepo.clearStatusCache()
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit
|
||||
override fun onActivityResumed(activity: Activity) = Unit
|
||||
override fun onActivityPaused(activity: Activity) = Unit
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
|
||||
override fun onActivityDestroyed(activity: Activity) = Unit
|
||||
})
|
||||
}
|
||||
|
||||
private suspend fun checkAutoUpdate(timeNow: Long = System.currentTimeMillis()) {
|
||||
if (container.settingsRepo.otherSettings.value.stateOfAutoUpdate) {
|
||||
val timeDelta = timeNow - container.settingsRepo.databaseState.value.updateTimestamp
|
||||
if (timeDelta > 172_800_000L) { // 48 hours in ms
|
||||
val sdf = SimpleDateFormat("d MMM yyyy - HH:mm:ss", Locale.getDefault())
|
||||
println("Started periodic data update on ${sdf.format(Date())}")
|
||||
container.databaseRepo.updateFromRemote()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatic LoTW grid sync, mirroring [checkAutoUpdate]: checked on every
|
||||
* app start, but only pulls when due — LoTW credentials configured, the
|
||||
* LoTW auto-sync toggle on, at least one prior (manual) sync, and not
|
||||
* already synced today (ARRL limits report pulls, ~once a day per account).
|
||||
* Incremental when the callsign is unchanged; a callsign change falls back
|
||||
* to a full replace so no stale grids from another account linger.
|
||||
* Failures are silent and simply retried on the next app start — the manual
|
||||
* sync button still surfaces the explicit cause to the user.
|
||||
*/
|
||||
private suspend fun checkLoTWAutoSync(timeNow: Long = System.currentTimeMillis()) {
|
||||
val settingsRepo = container.settingsRepo
|
||||
val lotwSettings = settingsRepo.lotwSettings.value
|
||||
if (!shouldAutoSyncLoTW(
|
||||
isConfigured = lotwSettings.isConfigured,
|
||||
autoLotwSyncEnabled = settingsRepo.otherSettings.value.stateOfAutoLotwSync,
|
||||
lastSyncDate = settingsRepo.getLastLotwSyncDate(),
|
||||
today = lotwSyncToday(timeNow)
|
||||
)
|
||||
) return
|
||||
val callsign = lotwSettings.callsign.trim().uppercase()
|
||||
val mode = resolveLoTWSyncMode(settingsRepo.getLastLotwSyncCallsign(), callsign, requested = null)
|
||||
val since = if (mode == LoTWSyncMode.Incremental) lotwCursorApi(settingsRepo.getLastLotwSyncDate()) else ""
|
||||
println("Started periodic LoTW grid sync (${mode.name.lowercase()})")
|
||||
val result = container.lotwRepo.fetchConfirmedGridQsos(callsign, lotwSettings.password, since)
|
||||
if (result is LoTWResult.Success) {
|
||||
val count = applyLoTWGridResult(settingsRepo, result, mode, callsign, timeNow)
|
||||
println("Periodic LoTW grid sync finished: $count worked grids")
|
||||
} else {
|
||||
println("Periodic LoTW grid sync skipped (${result::class.simpleName})")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat
|
||||
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults
|
||||
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
|
||||
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
||||
import com.rtbishop.look4sat.core.domain.repository.MutualPassData
|
||||
import com.rtbishop.look4sat.core.presentation.DeeplinkResolver
|
||||
import com.rtbishop.look4sat.core.presentation.ElevationThresholds
|
||||
import com.rtbishop.look4sat.core.presentation.LocalElevationThresholds
|
||||
import com.rtbishop.look4sat.core.presentation.RadarDestination
|
||||
import com.rtbishop.look4sat.core.presentation.Screen
|
||||
import com.rtbishop.look4sat.core.presentation.hasEnoughHeight
|
||||
import com.rtbishop.look4sat.core.presentation.hasEnoughWidth
|
||||
import com.rtbishop.look4sat.feature.map.MapDestination
|
||||
import com.rtbishop.look4sat.feature.map.MapFilterViewModel
|
||||
import com.rtbishop.look4sat.feature.mutual.MutualScreen
|
||||
import com.rtbishop.look4sat.feature.mutual.MutualViewModel
|
||||
import com.rtbishop.look4sat.feature.passes.PassesDestination
|
||||
import com.rtbishop.look4sat.feature.radar.RadarDestination
|
||||
import com.rtbishop.look4sat.feature.satellites.SatellitesDestination
|
||||
import com.rtbishop.look4sat.feature.settings.SettingsDestination
|
||||
import com.rtbishop.look4sat.feature.status.SatStatusDestination
|
||||
|
||||
@Composable
|
||||
fun NavRoot(deeplink: String? = null) {
|
||||
val rootBackStack = rememberNavBackStack(Screen.Passes)
|
||||
val deeplinkResolver = DeeplinkResolver()
|
||||
LaunchedEffect(deeplink) {
|
||||
deeplink?.let { rootBackStack.add(deeplinkResolver.resolve(it)) }
|
||||
}
|
||||
val navigateBack: () -> Unit = { rootBackStack.removeLastOrNull() }
|
||||
val navigateToRadar: () -> Unit = { rootBackStack.add(RadarDestination) }
|
||||
// Incoming screen slides in from the right, outgoing drifts left at 1/3 speed (API35+ style)
|
||||
val pushTransition = slideInHorizontally(tween(300)) { it } togetherWith
|
||||
slideOutHorizontally(tween(300)) { -it / 3 }
|
||||
// Reverse: outgoing slides out to the right, incoming drifts in from the left
|
||||
val popTransition = slideInHorizontally(tween(300)) { -it / 3 } togetherWith
|
||||
slideOutHorizontally(tween(300)) { it }
|
||||
// Elevation color thresholds must be provided at the root level so that BOTH
|
||||
// the tab content (MainScreen) and the root-level RadarDestination see the
|
||||
// user's custom thresholds. RadarDestination is a sibling entry of MainScreen
|
||||
// in this NavDisplay, so a provider inside MainScreen never reaches it.
|
||||
val context = LocalContext.current
|
||||
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
||||
val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle()
|
||||
CompositionLocalProvider(
|
||||
LocalElevationThresholds provides ElevationThresholds(
|
||||
low = otherSettings.lowElevation,
|
||||
high = otherSettings.highElevation
|
||||
)
|
||||
) {
|
||||
NavDisplay(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
backStack = rootBackStack,
|
||||
onBack = navigateBack,
|
||||
transitionSpec = { pushTransition },
|
||||
popTransitionSpec = { popTransition },
|
||||
predictivePopTransitionSpec = { popTransition },
|
||||
entryDecorators = listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator(),
|
||||
rememberViewModelStoreNavEntryDecorator()
|
||||
),
|
||||
entryProvider = entryProvider {
|
||||
entry<Screen.Passes> {
|
||||
MainScreen(
|
||||
navigateToRadar = navigateToRadar
|
||||
)
|
||||
}
|
||||
entry<RadarDestination> {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
RadarDestination(navigateUp = navigateBack)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
navigateToRadar: () -> Unit = {}
|
||||
) {
|
||||
val backStack = rememberNavBackStack(Screen.Passes)
|
||||
val currentKey = backStack.lastOrNull()
|
||||
val navigateBack: () -> Unit = { backStack.removeLastOrNull() }
|
||||
val fadeTransition = fadeIn(animationSpec = tween(350)) togetherWith fadeOut(animationSpec = tween(350))
|
||||
// Map sits at position 4 from the left, matching the upstream nav order
|
||||
// (Satellites, Passes, AMSAT/Status, Map, ...), with the fork's Mutual tab
|
||||
// appended before Settings.
|
||||
val navItems = listOf(Screen.Satellites, Screen.Passes, Screen.AMSAT, Screen.Map, Screen.Mutual, Screen.Settings)
|
||||
|
||||
val context = LocalContext.current
|
||||
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
||||
val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle()
|
||||
val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle()
|
||||
// Activity-scoped so the mutual query results survive navigation to Radar and back
|
||||
val mutualViewModel: MutualViewModel = viewModel(
|
||||
viewModelStoreOwner = context as ViewModelStoreOwner,
|
||||
factory = MutualViewModel.factory(container)
|
||||
)
|
||||
// Activity-scoped so the map's award filter survives page switches;
|
||||
// resets to VUCC only on cold start (fresh process).
|
||||
val mapFilterViewModel: MapFilterViewModel = viewModel(
|
||||
viewModelStoreOwner = context as ViewModelStoreOwner,
|
||||
factory = MapFilterViewModel.factory()
|
||||
)
|
||||
|
||||
// Map grid-QSO dialog "Match" button: pre-fill the match page for that
|
||||
// grid and run the query BEFORE navigating, so the page's first frame
|
||||
// already contains the results (time-range card at the top, pass curves
|
||||
// right below — no scroll flicker). Navigation happens once the query
|
||||
// finishes (see the collector below).
|
||||
val mutualState by mutualViewModel.uiState.collectAsStateWithLifecycle()
|
||||
val matchCalculating = mutualState.pendingNavigation && mutualState.isCalculating
|
||||
|
||||
LaunchedEffect(mutualViewModel) {
|
||||
mutualViewModel.uiState.collect { state ->
|
||||
if (state.pendingNavigation && !state.isCalculating) {
|
||||
mutualViewModel.consumePendingNavigation()
|
||||
// Push Mutual on top of the Map entry instead of replacing the
|
||||
// stack (bottom-nav style): the system back gesture then pops
|
||||
// back to the map page, which is the page the user came from.
|
||||
if (backStack.lastOrNull() !is Screen.Mutual) {
|
||||
backStack.add(Screen.Mutual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalElevationThresholds provides ElevationThresholds(
|
||||
low = otherSettings.lowElevation,
|
||||
high = otherSettings.highElevation
|
||||
)
|
||||
) {
|
||||
NavigationSuiteScaffold(
|
||||
navigationSuiteItems = {
|
||||
navItems.forEach { screen ->
|
||||
val isSelected = when (currentKey) {
|
||||
is Screen.Satellites -> screen is Screen.Satellites
|
||||
is Screen.Passes -> screen is Screen.Passes
|
||||
is Screen.Map -> screen is Screen.Map
|
||||
is Screen.AMSAT -> screen is Screen.AMSAT
|
||||
is Screen.Mutual -> screen is Screen.Mutual
|
||||
is Screen.Settings -> screen is Screen.Settings
|
||||
else -> false
|
||||
}
|
||||
item(
|
||||
icon = { Icon(painterResource(screen.iconResId), stringResource(screen.titleResId)) },
|
||||
label = { Text(stringResource(screen.titleResId)) },
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
if (isSelected) return@item
|
||||
while (backStack.size > 1) backStack.removeAt(backStack.size - 1)
|
||||
if (screen !is Screen.Passes) backStack.add(screen)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationSuiteColors = NavigationSuiteDefaults.colors(
|
||||
navigationRailContainerColor = MaterialTheme.colorScheme.surfaceContainer
|
||||
),
|
||||
layoutType = when {
|
||||
!hasEnoughHeight() && hasEnoughWidth() -> NavigationSuiteType.NavigationRail
|
||||
!hasEnoughWidth() -> NavigationSuiteType.ShortNavigationBarCompact
|
||||
else -> NavigationSuiteType.ShortNavigationBarMedium
|
||||
}
|
||||
) {
|
||||
Column {
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
modifier = Modifier.weight(1f),
|
||||
onBack = navigateBack,
|
||||
transitionSpec = { fadeTransition },
|
||||
popTransitionSpec = { fadeTransition },
|
||||
predictivePopTransitionSpec = { fadeTransition },
|
||||
entryDecorators = listOf(
|
||||
// Required for saving Compose state per entry
|
||||
rememberSaveableStateHolderNavEntryDecorator(),
|
||||
// Required for ViewModel scoping per entry
|
||||
rememberViewModelStoreNavEntryDecorator()
|
||||
),
|
||||
entryProvider = entryProvider {
|
||||
entry<Screen.Satellites> {
|
||||
SatellitesDestination(navigateUp = navigateBack)
|
||||
}
|
||||
entry<Screen.Passes> {
|
||||
PassesDestination(
|
||||
navigateToRadar = { catNum, aosTime ->
|
||||
container.setMutualPassData(MutualPassData())
|
||||
container.satelliteRepo.selectPass(catNum, aosTime)
|
||||
navigateToRadar()
|
||||
}
|
||||
)
|
||||
}
|
||||
entry<Screen.Map> {
|
||||
MapDestination(
|
||||
mapFilterViewModel = mapFilterViewModel,
|
||||
matchCalculating = matchCalculating,
|
||||
onMatchGrid = { grid ->
|
||||
// Grid-QSO dialog "Match" button: pre-fill
|
||||
// the match page for that grid and start the
|
||||
// query. Navigation to the match page is
|
||||
// handled by the uiState collector above,
|
||||
// once the query finishes.
|
||||
mutualViewModel.prefillMatchFromGrid(grid)
|
||||
}
|
||||
)
|
||||
}
|
||||
entry<Screen.Mutual> {
|
||||
MutualScreen(
|
||||
viewModel = mutualViewModel,
|
||||
navigateToRadar = { catNum, aosTime, pass ->
|
||||
container.setMutualPassData(pass ?: MutualPassData())
|
||||
container.satelliteRepo.selectPass(catNum, aosTime)
|
||||
navigateToRadar()
|
||||
}
|
||||
)
|
||||
}
|
||||
entry<Screen.AMSAT> { SatStatusDestination() }
|
||||
entry<Screen.Settings> {
|
||||
SettingsDestination()
|
||||
}
|
||||
}
|
||||
)
|
||||
// Radio tracking status banner
|
||||
if (trackingState.isActive) {
|
||||
val infiniteTransition = rememberInfiniteTransition(label = "trackingPulse")
|
||||
val alpha by infiniteTransition.animateFloat(
|
||||
initialValue = 1f, targetValue = 0.4f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(1000, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
), label = "pulseAlpha"
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||
.clickable {
|
||||
val pass = trackingState.currentPass
|
||||
if (pass != null) {
|
||||
container.setMutualPassData(MutualPassData())
|
||||
container.satelliteRepo.selectPass(pass.catNum, pass.aosTime)
|
||||
navigateToRadar()
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF4CAF50).copy(alpha = alpha))
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Tracking: ${trackingState.currentPass?.name ?: ""}",
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
val txOk = if (trackingState.txConnected) "TX" else ""
|
||||
val rxOk = if (trackingState.rxConnected) "RX" else ""
|
||||
Text(
|
||||
text = listOf(txOk, rxOk).filter { it.isNotBlank() }.joinToString("/"),
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.framework
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.location.LocationListener
|
||||
import android.location.LocationManager
|
||||
import android.os.Bundle
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.rtbishop.look4sat.R
|
||||
import com.rtbishop.look4sat.domain.ILocationManager
|
||||
import com.rtbishop.look4sat.domain.ISettingsManager
|
||||
import com.rtbishop.look4sat.domain.model.DataState
|
||||
import com.rtbishop.look4sat.domain.predict.GeoPos
|
||||
import com.rtbishop.look4sat.utility.QthConverter
|
||||
import com.rtbishop.look4sat.utility.round
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class LocationManager @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val manager: LocationManager,
|
||||
private val settings: ISettingsManager,
|
||||
) : LocationListener, ILocationManager {
|
||||
|
||||
private val providerDef = LocationManager.PASSIVE_PROVIDER
|
||||
private val providerNet = LocationManager.NETWORK_PROVIDER
|
||||
private val providerGps = LocationManager.GPS_PROVIDER
|
||||
private val locationCoarse = Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
private val locationFine = Manifest.permission.ACCESS_FINE_LOCATION
|
||||
private val _stationPosition = MutableStateFlow<DataState<GeoPos>>(DataState.Handled)
|
||||
private var currentLocator = settings.loadStationLocator()
|
||||
private var currentPosition = settings.loadStationPosition()
|
||||
|
||||
override val stationPosition: StateFlow<DataState<GeoPos>> = _stationPosition
|
||||
|
||||
override fun getStationLocator(): String = currentLocator
|
||||
|
||||
override fun getStationPosition(): GeoPos = currentPosition
|
||||
|
||||
override fun setStationPosition(latitude: Double, longitude: Double) {
|
||||
if (QthConverter.isValidPosition(latitude, longitude)) {
|
||||
val tempLon = if (longitude > 180.0) longitude - 180 else longitude
|
||||
val newLat = latitude.round(4)
|
||||
val newLon = tempLon.round(4)
|
||||
QthConverter.positionToQth(newLat, newLon)?.let { locator ->
|
||||
currentLocator = locator
|
||||
currentPosition = GeoPos(newLat, newLon)
|
||||
settings.saveStationLocator(locator)
|
||||
settings.saveStationPosition(currentPosition)
|
||||
_stationPosition.value = DataState.Success(currentPosition)
|
||||
}
|
||||
} else {
|
||||
_stationPosition.value =
|
||||
DataState.Error(context.getString(R.string.location_manual_error))
|
||||
}
|
||||
}
|
||||
|
||||
override fun setPositionFromGps() {
|
||||
val result = ContextCompat.checkSelfPermission(context, locationFine)
|
||||
if (manager.isProviderEnabled(providerGps) && result == PackageManager.PERMISSION_GRANTED) {
|
||||
val location = manager.getLastKnownLocation(providerDef)
|
||||
if (location != null) {
|
||||
setStationPosition(location.latitude, location.longitude)
|
||||
} else {
|
||||
_stationPosition.value = DataState.Loading
|
||||
manager.requestLocationUpdates(providerGps, 0L, 0f, this)
|
||||
}
|
||||
} else {
|
||||
_stationPosition.value = DataState.Error(context.getString(R.string.location_null))
|
||||
}
|
||||
}
|
||||
|
||||
override fun setPositionFromNet() {
|
||||
val result = ContextCompat.checkSelfPermission(context, locationCoarse)
|
||||
if (manager.isProviderEnabled(providerNet) && result == PackageManager.PERMISSION_GRANTED) {
|
||||
val location = manager.getLastKnownLocation(providerDef)
|
||||
if (location != null) {
|
||||
setStationPosition(location.latitude, location.longitude)
|
||||
} else {
|
||||
_stationPosition.value = DataState.Loading
|
||||
manager.requestLocationUpdates(providerNet, 0L, 0f, this)
|
||||
}
|
||||
} else {
|
||||
_stationPosition.value = DataState.Error(context.getString(R.string.location_null))
|
||||
}
|
||||
}
|
||||
|
||||
override fun setPositionFromQth(locator: String) {
|
||||
val position = QthConverter.qthToPosition(locator)
|
||||
if (position != null) {
|
||||
currentLocator = locator
|
||||
currentPosition = position
|
||||
settings.saveStationLocator(locator)
|
||||
settings.saveStationPosition(currentPosition)
|
||||
_stationPosition.value = DataState.Success(currentPosition)
|
||||
} else {
|
||||
_stationPosition.value = DataState.Error(context.getString(R.string.location_qth_error))
|
||||
}
|
||||
}
|
||||
|
||||
override fun setPositionHandled() {
|
||||
_stationPosition.value = DataState.Handled
|
||||
}
|
||||
|
||||
override fun onLocationChanged(location: Location) {
|
||||
manager.removeUpdates(this)
|
||||
setStationPosition(location.latitude, location.longitude)
|
||||
}
|
||||
|
||||
override fun onProviderEnabled(provider: String) {}
|
||||
|
||||
override fun onProviderDisabled(provider: String) {}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.framework
|
||||
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import com.rtbishop.look4sat.domain.predict.RAD2DEG
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.math.round
|
||||
|
||||
@Singleton
|
||||
class OrientationManager @Inject constructor(private val sensorManager: SensorManager) :
|
||||
SensorEventListener {
|
||||
|
||||
interface OrientationListener {
|
||||
fun onOrientationChanged(azimuth: Float, pitch: Float, roll: Float)
|
||||
}
|
||||
|
||||
private val sensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
|
||||
private val rotationMatrix = FloatArray(9)
|
||||
private val orientationValues = FloatArray(3)
|
||||
private var sensorAccuracy: Int = SensorManager.SENSOR_STATUS_UNRELIABLE
|
||||
private var orientationListener: OrientationListener? = null
|
||||
|
||||
fun startListening(listener: OrientationListener) {
|
||||
if (sensor != null && orientationListener !== listener) {
|
||||
orientationListener = listener
|
||||
sensorManager.registerListener(this, sensor, 16000)
|
||||
} else return
|
||||
}
|
||||
|
||||
fun stopListening() {
|
||||
sensorManager.unregisterListener(this)
|
||||
orientationListener = null
|
||||
}
|
||||
|
||||
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
|
||||
sensorAccuracy = accuracy
|
||||
}
|
||||
|
||||
override fun onSensorChanged(event: SensorEvent) {
|
||||
when {
|
||||
orientationListener == null -> return
|
||||
sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE -> return
|
||||
event.sensor == sensor -> updateOrientation(event.values)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateOrientation(rotationVector: FloatArray) {
|
||||
SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVector)
|
||||
SensorManager.getOrientation(rotationMatrix, orientationValues)
|
||||
val azimuth = (orientationValues[0] * RAD2DEG).toFloat()
|
||||
val pitch = (orientationValues[1] * RAD2DEG).toFloat()
|
||||
val roll = (orientationValues[2] * RAD2DEG).toFloat()
|
||||
val magneticAzimuth = (azimuth + 360f) % 360f
|
||||
val roundedAzimuth = round(magneticAzimuth * 10) / 10
|
||||
val roundedPitch = round(pitch * 10) / 10
|
||||
val roundedRoll = round(roll * 10) / 10
|
||||
orientationListener?.onOrientationChanged(roundedAzimuth, roundedPitch, roundedRoll)
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.framework
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.rtbishop.look4sat.domain.ISettingsManager
|
||||
import com.rtbishop.look4sat.domain.predict.GeoPos
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class SettingsManager @Inject constructor(private val prefs: SharedPreferences) : ISettingsManager {
|
||||
|
||||
companion object {
|
||||
const val keyModes = "satModes"
|
||||
const val keyCompass = "compass"
|
||||
const val keyRadarSweep = "radarSweep"
|
||||
const val keyTimeUTC = "timeUTC"
|
||||
const val keyLastUpdateTime = "lastUpdateTime"
|
||||
const val keyAutoUpdateEnabled = "autoUpdateEnabled"
|
||||
const val keyHoursAhead = "hoursAhead"
|
||||
const val keyMinElevation = "minElevation"
|
||||
const val keyRotator = "isRotatorEnabled"
|
||||
const val keyRotatorAddress = "rotatorAddress"
|
||||
const val keyRotatorPort = "rotatorPort"
|
||||
const val keyBTEnabled = "isBTEnabled"
|
||||
const val keyBTDeviceName = "BTDeviceName"
|
||||
const val keyBTDeviceAddr = "BTDeviceAddr"
|
||||
const val keyBTFormat = "BTFormat"
|
||||
const val keyLatitude = "stationLat"
|
||||
const val keyLongitude = "stationLon"
|
||||
const val keyLocator = "stationQTH"
|
||||
const val keySelection = "selection"
|
||||
}
|
||||
|
||||
override fun loadStationLocator(): String {
|
||||
return prefs.getString(keyLocator, null) ?: "null"
|
||||
}
|
||||
|
||||
override fun saveStationLocator(locator: String) {
|
||||
prefs.edit { putString(keyLocator, locator) }
|
||||
}
|
||||
|
||||
override fun loadStationPosition(): GeoPos {
|
||||
val defaultSP = "0.0"
|
||||
val latitude = prefs.getString(keyLatitude, null) ?: defaultSP
|
||||
val longitude = prefs.getString(keyLongitude, null) ?: defaultSP
|
||||
return GeoPos(latitude.toDouble(), longitude.toDouble())
|
||||
}
|
||||
|
||||
override fun saveStationPosition(position: GeoPos) {
|
||||
prefs.edit {
|
||||
putString(keyLatitude, position.lat.toString())
|
||||
putString(keyLongitude, position.lon.toString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun saveEntriesSelection(catnums: List<Int>) {
|
||||
val stringList = catnums.map { catnum -> catnum.toString() }
|
||||
prefs.edit { putStringSet(keySelection, stringList.toSet()) }
|
||||
}
|
||||
|
||||
override fun loadEntriesSelection(): List<Int> {
|
||||
val catnums = prefs.getStringSet(keySelection, emptySet())?.map { catnum -> catnum.toInt() }
|
||||
return catnums?.sorted() ?: emptyList()
|
||||
}
|
||||
|
||||
override fun saveSatType(type: String, catnums: List<Int>) {
|
||||
val stringList = catnums.map { catnum -> catnum.toString() }
|
||||
prefs.edit { putStringSet("type$type", stringList.toSet()) }
|
||||
}
|
||||
|
||||
override fun loadSatType(type: String): List<Int> {
|
||||
val catnums = prefs.getStringSet("type$type", emptySet())?.map { catnum -> catnum.toInt() }
|
||||
return catnums ?: emptyList()
|
||||
}
|
||||
|
||||
override fun getHoursAhead(): Int {
|
||||
return prefs.getInt(keyHoursAhead, 24)
|
||||
}
|
||||
|
||||
override fun setHoursAhead(hoursAhead: Int) {
|
||||
prefs.edit { putInt(keyHoursAhead, hoursAhead) }
|
||||
}
|
||||
|
||||
override fun getMinElevation(): Double {
|
||||
return prefs.getDouble(keyMinElevation, 16.0)
|
||||
}
|
||||
|
||||
override fun setMinElevation(minElevation: Double) {
|
||||
prefs.edit { putDouble(keyMinElevation, minElevation) }
|
||||
}
|
||||
|
||||
override fun getUseUTC(): Boolean {
|
||||
return prefs.getBoolean(keyTimeUTC, false)
|
||||
}
|
||||
|
||||
override fun setUseUTC(value: Boolean) {
|
||||
prefs.edit { putBoolean(keyTimeUTC, value) }
|
||||
}
|
||||
|
||||
override fun getLastUpdateTime(): Long {
|
||||
return prefs.getLong(keyLastUpdateTime, 0L)
|
||||
}
|
||||
|
||||
override fun setLastUpdateTime(updateTime: Long) {
|
||||
prefs.edit { putLong(keyLastUpdateTime, updateTime) }
|
||||
}
|
||||
|
||||
override fun getAutoUpdateEnabled(): Boolean {
|
||||
return prefs.getBoolean(keyAutoUpdateEnabled, true)
|
||||
}
|
||||
|
||||
override fun setAutoUpdateEnabled(value: Boolean) {
|
||||
prefs.edit { putBoolean(keyAutoUpdateEnabled, value) }
|
||||
}
|
||||
|
||||
override fun getUseCompass(): Boolean {
|
||||
return prefs.getBoolean(keyCompass, true)
|
||||
}
|
||||
|
||||
override fun setUseCompass(value: Boolean) {
|
||||
prefs.edit { putBoolean(keyCompass, value) }
|
||||
}
|
||||
|
||||
override fun getShowSweep(): Boolean {
|
||||
return prefs.getBoolean(keyRadarSweep, true)
|
||||
}
|
||||
|
||||
override fun setShowSweep(value: Boolean) {
|
||||
prefs.edit { putBoolean(keyRadarSweep, value) }
|
||||
}
|
||||
|
||||
override fun saveModesSelection(modes: List<String>) {
|
||||
prefs.edit { putStringSet(keyModes, modes.toSet()) }
|
||||
}
|
||||
|
||||
override fun loadModesSelection(): List<String> {
|
||||
return prefs.getStringSet(keyModes, null)?.toList()?.sorted() ?: emptyList()
|
||||
}
|
||||
|
||||
override fun getRotatorEnabled(): Boolean {
|
||||
return prefs.getBoolean(keyRotator, false)
|
||||
}
|
||||
|
||||
override fun setRotatorEnabled(value: Boolean) {
|
||||
prefs.edit { putBoolean(keyRotator, value) }
|
||||
}
|
||||
|
||||
override fun getRotatorServer(): String {
|
||||
return prefs.getString(keyRotatorAddress, null) ?: "127.0.0.1"
|
||||
}
|
||||
|
||||
override fun setRotatorServer(value: String) {
|
||||
prefs.edit { putString(keyRotatorAddress, value) }
|
||||
}
|
||||
|
||||
override fun getRotatorPort(): String {
|
||||
return prefs.getString(keyRotatorPort, null) ?: "4533"
|
||||
}
|
||||
|
||||
override fun setRotatorPort(value: String) {
|
||||
prefs.edit { putString(keyRotatorPort, value) }
|
||||
}
|
||||
|
||||
override fun getBTEnabled(): Boolean {
|
||||
return prefs.getBoolean(keyBTEnabled, false)
|
||||
}
|
||||
|
||||
override fun setBTEnabled(value: Boolean) {
|
||||
prefs.edit { putBoolean(keyBTEnabled, value) }
|
||||
}
|
||||
|
||||
override fun getBTDeviceAddr(): String {
|
||||
return prefs.getString(keyBTDeviceAddr, null) ?: "00:0C:BF:13:80:5D"
|
||||
}
|
||||
|
||||
override fun setBTDeviceAddr(value: String) {
|
||||
prefs.edit { putString(keyBTDeviceAddr, value) }
|
||||
}
|
||||
|
||||
override fun getBTDeviceName(): String {
|
||||
return prefs.getString(keyBTDeviceName, null) ?: "Default"
|
||||
}
|
||||
|
||||
override fun setBTDeviceName(value: String) {
|
||||
prefs.edit { putString(keyBTDeviceName, value) }
|
||||
}
|
||||
|
||||
override fun getBTFormat(): String {
|
||||
return prefs.getString(keyBTFormat, null) ?: "W\$AZ \$EL"
|
||||
}
|
||||
|
||||
override fun setBTFormat(value: String) {
|
||||
prefs.edit { putString(keyBTFormat, value) }
|
||||
}
|
||||
|
||||
private fun SharedPreferences.getDouble(key: String, default: Double): Double {
|
||||
return Double.fromBits(getLong(key, default.toRawBits()))
|
||||
}
|
||||
|
||||
private fun SharedPreferences.Editor.putDouble(key: String, double: Double) {
|
||||
putLong(key, double.toRawBits())
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.framework.data
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import com.rtbishop.look4sat.domain.data.IFileDataSource
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.InputStream
|
||||
|
||||
class FileDataSource(
|
||||
private val contentResolver: ContentResolver,
|
||||
private val ioDispatcher: CoroutineDispatcher
|
||||
) : IFileDataSource {
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
override suspend fun getDataStream(uri: String): InputStream? {
|
||||
return withContext(ioDispatcher) { contentResolver.openInputStream(Uri.parse(uri)) }
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.framework.data
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.rtbishop.look4sat.framework.data.dao.EntriesDao
|
||||
import com.rtbishop.look4sat.framework.data.dao.RadiosDao
|
||||
import com.rtbishop.look4sat.framework.model.SatEntry
|
||||
import com.rtbishop.look4sat.framework.model.SatRadio
|
||||
|
||||
@Database(entities = [SatEntry::class, SatRadio::class], version = 2, exportSchema = true)
|
||||
abstract class LocalDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun entriesDao(): EntriesDao
|
||||
|
||||
abstract fun radiosDao(): RadiosDao
|
||||
}
|
||||
|
||||
val MIGRATION_1_2 = object : Migration(1, 2) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE entries_backup (name TEXT NOT NULL, epoch REAL NOT NULL, meanmo REAL NOT NULL, eccn REAL NOT NULL, incl REAL NOT NULL, raan REAL NOT NULL, argper REAL NOT NULL, meanan REAL NOT NULL, catnum INTEGER NOT NULL, bstar REAL NOT NULL, xincl REAL NOT NULL, xnodeo REAL NOT NULL, omegao REAL NOT NULL, xmo REAL NOT NULL, xno REAL NOT NULL, orbitalPeriod REAL NOT NULL, isDeepSpace INTEGER NOT NULL, comment TEXT, PRIMARY KEY(catnum))")
|
||||
database.execSQL("INSERT INTO entries_backup (name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar, xincl, xnodeo, omegao, xmo, xno, orbitalPeriod, isDeepSpace, comment) SELECT name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar, xincl, xnodeo, omegao, xmo, xno, 1440 / meanmo, 1440 / meanmo >= 225.0, comment FROM entries")
|
||||
database.execSQL("DROP TABLE entries")
|
||||
database.execSQL("ALTER TABLE entries_backup RENAME TO entries")
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.framework.data
|
||||
|
||||
import com.rtbishop.look4sat.domain.data.ILocalEntrySource
|
||||
import com.rtbishop.look4sat.domain.predict.Satellite
|
||||
import com.rtbishop.look4sat.framework.data.dao.EntriesDao
|
||||
import com.rtbishop.look4sat.domain.model.SatEntry as DomainEntry
|
||||
import com.rtbishop.look4sat.domain.model.SatItem as DomainItem
|
||||
import com.rtbishop.look4sat.framework.model.SatEntry as FrameworkEntry
|
||||
import com.rtbishop.look4sat.framework.model.SatItem as FrameworkItem
|
||||
|
||||
class LocalEntrySource(private val entriesDao: EntriesDao) : ILocalEntrySource {
|
||||
|
||||
override fun getEntriesTotal() = entriesDao.getEntriesTotal()
|
||||
|
||||
override suspend fun getEntriesWithModes(): List<DomainItem> {
|
||||
return entriesDao.getEntriesWithModes().toDomainItems()
|
||||
}
|
||||
|
||||
override suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite> {
|
||||
val selectedSatellites = mutableListOf<Satellite>()
|
||||
ids.chunked(999).forEach { idsPart ->
|
||||
val entries = entriesDao.getEntriesWithIds(idsPart)
|
||||
selectedSatellites.addAll(entries.map { entry -> entry.data.getSatellite() })
|
||||
}
|
||||
return selectedSatellites
|
||||
}
|
||||
|
||||
override suspend fun insertEntries(entries: List<DomainEntry>) {
|
||||
entriesDao.insertEntries(entries.toFrameworkEntries())
|
||||
}
|
||||
|
||||
override suspend fun deleteEntries() = entriesDao.deleteEntries()
|
||||
|
||||
private fun DomainEntry.toFramework() = FrameworkEntry(this.data, this.comment)
|
||||
|
||||
private fun FrameworkItem.toDomain() = DomainItem(this.catnum, this.name, this.modes, false)
|
||||
|
||||
private fun List<DomainEntry>.toFrameworkEntries() = this.map { entry -> entry.toFramework() }
|
||||
|
||||
private fun List<FrameworkItem>.toDomainItems() = this.map { item -> item.toDomain() }
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.framework.data
|
||||
|
||||
import com.rtbishop.look4sat.domain.model.SatRadio as DomainRadio
|
||||
import com.rtbishop.look4sat.framework.model.SatRadio as FrameworkRadio
|
||||
import com.rtbishop.look4sat.domain.data.ILocalRadioSource
|
||||
import com.rtbishop.look4sat.framework.data.dao.RadiosDao
|
||||
|
||||
class LocalRadioSource(private val radiosDao: RadiosDao) : ILocalRadioSource {
|
||||
|
||||
override fun getRadiosTotal() = radiosDao.getRadiosTotal()
|
||||
|
||||
override suspend fun getRadiosWithId(id: Int): List<DomainRadio> {
|
||||
return radiosDao.getRadiosWithId(id).toDomainRadios()
|
||||
}
|
||||
|
||||
override suspend fun insertRadios(radios: List<DomainRadio>) {
|
||||
radiosDao.deleteRadios()
|
||||
radiosDao.insertRadios(radios.toFrameworkRadios())
|
||||
}
|
||||
|
||||
override suspend fun deleteRadios() = radiosDao.deleteRadios()
|
||||
|
||||
private fun DomainRadio.toFramework() = FrameworkRadio(
|
||||
this.uuid, this.info, this.isAlive, this.downlink, this.uplink,
|
||||
this.mode, this.isInverted, this.catnum, this.comment
|
||||
)
|
||||
|
||||
private fun FrameworkRadio.toDomain() = DomainRadio(
|
||||
this.uuid, this.info, this.isAlive, this.downlink, this.uplink,
|
||||
this.mode, this.isInverted, this.catnum, this.comment
|
||||
)
|
||||
|
||||
private fun List<DomainRadio>.toFrameworkRadios() = this.map { radio -> radio.toFramework() }
|
||||
|
||||
private fun List<FrameworkRadio>.toDomainRadios() = this.map { radio -> radio.toDomain() }
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.framework.data
|
||||
|
||||
import com.rtbishop.look4sat.domain.data.IRemoteDataSource
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.InputStream
|
||||
|
||||
class RemoteDataSource(
|
||||
private val httpClient: OkHttpClient, private val ioDispatcher: CoroutineDispatcher
|
||||
) : IRemoteDataSource {
|
||||
|
||||
override suspend fun getDataStream(url: String): InputStream? = withContext(ioDispatcher) {
|
||||
try {
|
||||
val request = Request.Builder().url(url).build()
|
||||
httpClient.newCall(request).execute().body?.byteStream()
|
||||
} catch (exception: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.framework.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import com.rtbishop.look4sat.framework.model.SatEntry
|
||||
import com.rtbishop.look4sat.framework.model.SatItem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface EntriesDao {
|
||||
|
||||
@Query("SELECT COUNT(*) FROM entries")
|
||||
fun getEntriesTotal(): Flow<Int>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT catnum, name FROM entries ORDER BY name ASC")
|
||||
suspend fun getEntriesWithModes(): List<SatItem>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM entries WHERE catnum IN (:selectedIds)")
|
||||
suspend fun getEntriesWithIds(selectedIds: List<Int>): List<SatEntry>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertEntries(entries: List<SatEntry>)
|
||||
|
||||
@Query("DELETE FROM entries")
|
||||
suspend fun deleteEntries()
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.framework.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import com.rtbishop.look4sat.framework.model.SatRadio
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface RadiosDao {
|
||||
|
||||
@Query("SELECT COUNT(*) FROM radios")
|
||||
fun getRadiosTotal(): Flow<Int>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM radios WHERE catnum = :id AND isAlive = 1")
|
||||
suspend fun getRadiosWithId(id: Int): List<SatRadio>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertRadios(radios: List<SatRadio>)
|
||||
|
||||
@Query("DELETE FROM radios")
|
||||
suspend fun deleteRadios()
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.framework.model
|
||||
|
||||
data class DataSource(var url: String = String())
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.framework.model
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import com.rtbishop.look4sat.domain.predict.OrbitalData
|
||||
|
||||
@Entity(tableName = "entries", primaryKeys = ["catnum"])
|
||||
data class SatEntry(@Embedded val data: OrbitalData, var comment: String? = null)
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.framework.model
|
||||
|
||||
import androidx.room.Relation
|
||||
|
||||
data class SatItem(
|
||||
val catnum: Int,
|
||||
val name: String,
|
||||
@Relation(
|
||||
parentColumn = "catnum",
|
||||
entity = SatRadio::class,
|
||||
entityColumn = "catnum",
|
||||
projection = ["mode"]
|
||||
)
|
||||
val modes: List<String>
|
||||
)
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.framework.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "radios")
|
||||
data class SatRadio(
|
||||
@PrimaryKey val uuid: String,
|
||||
val info: String,
|
||||
val isAlive: Boolean,
|
||||
var downlink: Long?,
|
||||
var uplink: Long?,
|
||||
val mode: String?,
|
||||
val isInverted: Boolean,
|
||||
val catnum: Int?,
|
||||
var comment: String? = null
|
||||
)
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.injection
|
||||
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.hardware.SensorManager
|
||||
import android.location.LocationManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object AppModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBluetoothManager(@ApplicationContext context: Context): BluetoothManager {
|
||||
return context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideLocationManager(@ApplicationContext context: Context): LocationManager {
|
||||
return context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSensorManager(@ApplicationContext context: Context): SensorManager {
|
||||
return context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
|
||||
return context.getSharedPreferences("default", Context.MODE_PRIVATE)
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.injection
|
||||
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.rtbishop.look4sat.domain.IDataRepository
|
||||
import com.rtbishop.look4sat.domain.ILocationManager
|
||||
import com.rtbishop.look4sat.domain.ISatelliteManager
|
||||
import com.rtbishop.look4sat.domain.ISettingsManager
|
||||
import com.rtbishop.look4sat.domain.data.DataRepository
|
||||
import com.rtbishop.look4sat.domain.predict.SatelliteManager
|
||||
import com.rtbishop.look4sat.framework.LocationManager
|
||||
import com.rtbishop.look4sat.framework.SettingsManager
|
||||
import com.rtbishop.look4sat.framework.data.FileDataSource
|
||||
import com.rtbishop.look4sat.framework.data.LocalDatabase
|
||||
import com.rtbishop.look4sat.framework.data.LocalEntrySource
|
||||
import com.rtbishop.look4sat.framework.data.LocalRadioSource
|
||||
import com.rtbishop.look4sat.framework.data.MIGRATION_1_2
|
||||
import com.rtbishop.look4sat.framework.data.RemoteDataSource
|
||||
import com.rtbishop.look4sat.presentation.radarScreen.BTReporter
|
||||
import com.rtbishop.look4sat.utility.DataParser
|
||||
import com.rtbishop.look4sat.utility.DataReporter
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object BaseModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDataRepository(
|
||||
@ApplicationContext context: Context,
|
||||
settings: ISettingsManager
|
||||
): IDataRepository {
|
||||
val db = Room.databaseBuilder(context, LocalDatabase::class.java, "Look4SatDb")
|
||||
.addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().build()
|
||||
val parser = DataParser(Dispatchers.Default)
|
||||
val fileSource = FileDataSource(context.contentResolver, Dispatchers.IO)
|
||||
val entries = LocalEntrySource(db.entriesDao())
|
||||
val radios = LocalRadioSource(db.radiosDao())
|
||||
val remoteSource = RemoteDataSource(OkHttpClient.Builder().build(), Dispatchers.IO)
|
||||
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
return DataRepository(parser, fileSource, entries, radios, remoteSource, scope, settings)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDataReporter(): DataReporter = DataReporter(CoroutineScope(Dispatchers.IO))
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBTReporter(manager: BluetoothManager): BTReporter {
|
||||
return BTReporter(manager, CoroutineScope(Dispatchers.IO))
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideLocationManager(manager: LocationManager): ILocationManager = manager
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSatelliteManager(): ISatelliteManager = SatelliteManager(Dispatchers.Default)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSettingsManager(manager: SettingsManager): ISettingsManager = manager
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.view.View
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.rtbishop.look4sat.databinding.ActivityMainBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityMainBinding.inflate(layoutInflater).root)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
val newConfig = Configuration(newBase?.resources?.configuration)
|
||||
newConfig.fontScale = 1.0f
|
||||
applyOverrideConfiguration(newConfig)
|
||||
super.attachBaseContext(newBase)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Fragment.getNavResult(@IdRes id: Int, key: String, onResult: (result: T) -> Unit) {
|
||||
val backStackEntry = findNavController().getBackStackEntry(id)
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME && backStackEntry.savedStateHandle.contains(key)) {
|
||||
backStackEntry.savedStateHandle.get<T>(key)?.let(onResult)
|
||||
backStackEntry.savedStateHandle.remove<T>(key)
|
||||
}
|
||||
}
|
||||
backStackEntry.lifecycle.addObserver(observer)
|
||||
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_DESTROY) backStackEntry.lifecycle.removeObserver(observer)
|
||||
})
|
||||
}
|
||||
|
||||
fun <T> Fragment.setNavResult(key: String, value: T) {
|
||||
findNavController().previousBackStackEntry?.savedStateHandle?.set(key, value)
|
||||
}
|
||||
|
||||
fun View.clickWithDebounce(debounceTime: Long = 875L, action: () -> Unit) {
|
||||
this.setOnClickListener(object : View.OnClickListener {
|
||||
private var lastClickTime: Long = 0
|
||||
|
||||
override fun onClick(v: View) {
|
||||
if (SystemClock.elapsedRealtime() - lastClickTime < debounceTime) return
|
||||
else action()
|
||||
lastClickTime = SystemClock.elapsedRealtime()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import com.rtbishop.look4sat.domain.IDataRepository
|
||||
import com.rtbishop.look4sat.framework.SettingsManager
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class MainApplication : Application() {
|
||||
|
||||
@Inject
|
||||
lateinit var repository: IDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var settingsManager: SettingsManager
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
checkAutoUpdate()
|
||||
}
|
||||
|
||||
private fun checkAutoUpdate() {
|
||||
if (settingsManager.getAutoUpdateEnabled()) {
|
||||
val timeDelta = System.currentTimeMillis() - settingsManager.getLastUpdateTime()
|
||||
if (timeDelta > 172800000) { // 48 hours in ms
|
||||
val sdf = SimpleDateFormat("d MMM yyyy - HH:mm:ss", Locale.getDefault())
|
||||
Log.d("AutoUpdate", "Started periodic data update on ${sdf.format(Date())}")
|
||||
repository.updateFromWeb()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.entriesScreen
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.rtbishop.look4sat.databinding.ItemEntryBinding
|
||||
import com.rtbishop.look4sat.domain.model.SatItem
|
||||
|
||||
class EntriesAdapter(private val clickListener: EntriesClickListener) :
|
||||
RecyclerView.Adapter<EntriesAdapter.SatItemHolder>() {
|
||||
|
||||
private val diffCallback = object : DiffUtil.ItemCallback<SatItem>() {
|
||||
override fun areItemsTheSame(oldItem: SatItem, newItem: SatItem): Boolean {
|
||||
return oldItem.catnum == newItem.catnum
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: SatItem, newItem: SatItem): Boolean {
|
||||
return oldItem.isSelected == newItem.isSelected
|
||||
}
|
||||
}
|
||||
private val differ = AsyncListDiffer(this, diffCallback)
|
||||
|
||||
fun submitList(items: List<SatItem>) {
|
||||
differ.submitList(items)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = differ.currentList.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SatItemHolder {
|
||||
return SatItemHolder.from(parent)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SatItemHolder, position: Int) {
|
||||
holder.bind(differ.currentList[position], clickListener)
|
||||
}
|
||||
|
||||
class SatItemHolder private constructor(private val binding: ItemEntryBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: SatItem, listener: EntriesClickListener) {
|
||||
binding.entryCheckbox.text = item.name
|
||||
binding.entryCheckbox.isChecked = item.isSelected
|
||||
binding.entryCheckbox.setOnClickListener {
|
||||
listener.updateSelection(listOf(item.catnum), item.isSelected.not())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(parent: ViewGroup): SatItemHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return SatItemHolder(ItemEntryBinding.inflate(inflater, parent, false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface EntriesClickListener {
|
||||
fun updateSelection(catNums: List<Int>, isSelected: Boolean)
|
||||
}
|
||||
}
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.entriesScreen
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.rtbishop.look4sat.R
|
||||
import com.rtbishop.look4sat.databinding.FragmentEntriesBinding
|
||||
import com.rtbishop.look4sat.domain.model.DataState
|
||||
import com.rtbishop.look4sat.domain.model.SatItem
|
||||
import com.rtbishop.look4sat.presentation.clickWithDebounce
|
||||
import com.rtbishop.look4sat.presentation.getNavResult
|
||||
import com.rtbishop.look4sat.presentation.setNavResult
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class EntriesFragment : Fragment(R.layout.fragment_entries) {
|
||||
|
||||
private val viewModel: EntriesViewModel by viewModels()
|
||||
private lateinit var binding: FragmentEntriesBinding
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val entriesAdapter = EntriesAdapter(viewModel)
|
||||
binding = FragmentEntriesBinding.bind(view).apply {
|
||||
entriesRecycler.apply {
|
||||
setHasFixedSize(true)
|
||||
adapter = entriesAdapter
|
||||
layoutManager = GridLayoutManager(requireContext(), 2)
|
||||
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
addItemDecoration(DividerItemDecoration(requireContext(), 1))
|
||||
addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
if (dy > 0 && entriesFab.visibility == View.VISIBLE) entriesFab.hide()
|
||||
else if (dy < 0 && entriesFab.visibility != View.VISIBLE) entriesFab.show()
|
||||
}
|
||||
})
|
||||
}
|
||||
entriesBtnBack.clickWithDebounce { findNavController().navigateUp() }
|
||||
entriesSearch.doOnTextChanged { text, _, _, _ -> viewModel.setQuery(text.toString()) }
|
||||
entriesBtnModes.clickWithDebounce {
|
||||
val direction = EntriesFragmentDirections.entriesToModes()
|
||||
findNavController().navigate(direction)
|
||||
}
|
||||
entriesBtnSelect.clickWithDebounce { viewModel.selectCurrentItems(true) }
|
||||
entriesBtnClear.clickWithDebounce { viewModel.selectCurrentItems(false) }
|
||||
val typeMessageFormat = requireContext().getString(R.string.types_message)
|
||||
val type = if (viewModel.getSatType().isNullOrBlank()) "All" else viewModel.getSatType()
|
||||
entriesTypeMessage.text = String.format(typeMessageFormat, type)
|
||||
entriesTypeCard.setOnClickListener { showSelectTypeDialog() }
|
||||
}
|
||||
viewModel.satData.observe(viewLifecycleOwner) { satData ->
|
||||
handleSatData(satData, entriesAdapter)
|
||||
binding.entriesFab.clickWithDebounce {
|
||||
setNavResult("selection", viewModel.saveSelection())
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
getNavResult<List<String>>(R.id.nav_entries, "modes") { modes ->
|
||||
viewModel.saveSelectedModes(modes)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSatData(dataState: DataState<List<SatItem>>, entriesAdapter: EntriesAdapter) {
|
||||
when (dataState) {
|
||||
is DataState.Success -> {
|
||||
entriesAdapter.submitList(dataState.data)
|
||||
binding.entriesProgress.visibility = View.INVISIBLE
|
||||
}
|
||||
is DataState.Loading -> {
|
||||
binding.entriesProgress.visibility = View.VISIBLE
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSelectTypeDialog() {
|
||||
val satelliteTypes = viewModel.satTypes.toTypedArray()
|
||||
val selectedValue = satelliteTypes.indexOf(viewModel.getSatType())
|
||||
val typeFormat = requireContext().getString(R.string.types_message)
|
||||
AlertDialog.Builder(requireContext()).apply {
|
||||
setTitle(R.string.types_title)
|
||||
setSingleChoiceItems(satelliteTypes, selectedValue) { dialog, index ->
|
||||
val selectedItem = satelliteTypes[index]
|
||||
binding.entriesTypeMessage.text = String.format(typeFormat, selectedItem)
|
||||
viewModel.setSatType(selectedItem)
|
||||
dialog.dismiss()
|
||||
}
|
||||
create()
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
-120
@@ -1,120 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.entriesScreen
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import com.rtbishop.look4sat.domain.IDataRepository
|
||||
import com.rtbishop.look4sat.domain.ISettingsManager
|
||||
import com.rtbishop.look4sat.domain.model.DataState
|
||||
import com.rtbishop.look4sat.domain.model.SatItem
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class EntriesViewModel @Inject constructor(
|
||||
private val repository: IDataRepository,
|
||||
private val settings: ISettingsManager
|
||||
) : ViewModel(), EntriesAdapter.EntriesClickListener {
|
||||
|
||||
private val satType = MutableLiveData(String())
|
||||
private val transModes = MutableLiveData(settings.loadModesSelection())
|
||||
private val currentQuery = MutableLiveData(String())
|
||||
private val itemsFromRepo = liveData {
|
||||
delay(125)
|
||||
emit(loadEntriesWithSelection())
|
||||
} as MutableLiveData
|
||||
private val itemsWithType = satType.switchMap { type ->
|
||||
itemsFromRepo.map { items -> filterByType(items, type) }
|
||||
}
|
||||
private val itemsWithModes = transModes.switchMap { modes ->
|
||||
itemsWithType.map { items -> filterByModes(items, modes) }
|
||||
}
|
||||
private val itemsWithQuery = currentQuery.switchMap { query ->
|
||||
itemsWithModes.map { items -> filterByQuery(items, query) }
|
||||
}
|
||||
val satData = itemsWithQuery.map { items -> DataState.Success(items) }
|
||||
val satTypes: List<String> = settings.sourcesMap.keys.sorted()
|
||||
|
||||
fun getSatType(): String? = satType.value
|
||||
|
||||
fun setSatType(type: String) {
|
||||
satType.value = type
|
||||
}
|
||||
|
||||
fun selectCurrentItems(selectAll: Boolean) {
|
||||
itemsWithQuery.value?.let { itemsWithQuery ->
|
||||
updateSelection(itemsWithQuery.map { item -> item.catnum }, selectAll)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSelectedModes(modes: List<String>) {
|
||||
transModes.value = modes
|
||||
settings.saveModesSelection(modes)
|
||||
}
|
||||
|
||||
fun setQuery(query: String) {
|
||||
currentQuery.value = query
|
||||
}
|
||||
|
||||
fun saveSelection(): List<Int> {
|
||||
return itemsFromRepo.value?.let { itemsAll ->
|
||||
itemsAll.filter { item -> item.isSelected }.map { item -> item.catnum }
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
override fun updateSelection(catNums: List<Int>, isSelected: Boolean) {
|
||||
itemsFromRepo.value?.let { itemsAll ->
|
||||
val copiedList = itemsAll.map { item -> item.copy() }
|
||||
catNums.forEach { catnum ->
|
||||
copiedList.find { item -> item.catnum == catnum }?.isSelected = isSelected
|
||||
}
|
||||
itemsFromRepo.value = copiedList
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadEntriesWithSelection(): List<SatItem> {
|
||||
val selectedIds = settings.loadEntriesSelection()
|
||||
return repository.getEntriesWithModes().onEach { it.isSelected = it.catnum in selectedIds }
|
||||
}
|
||||
|
||||
private fun filterByType(items: List<SatItem>, type: String): List<SatItem> {
|
||||
if (type == "All") return items
|
||||
val catnums = settings.loadSatType(type)
|
||||
if (catnums.isEmpty()) return items
|
||||
return items.filter { item -> item.catnum in catnums }
|
||||
}
|
||||
|
||||
private fun filterByModes(items: List<SatItem>, modes: List<String>): List<SatItem> {
|
||||
if (modes.isEmpty()) return items
|
||||
return items.filter { item -> item.modes.any { mode -> mode in modes } }
|
||||
}
|
||||
|
||||
private fun filterByQuery(items: List<SatItem>, query: String): List<SatItem> {
|
||||
if (query.isBlank()) return items
|
||||
return try {
|
||||
items.filter { it.catnum == query.toInt() }
|
||||
} catch (e: Exception) {
|
||||
items.filter { item ->
|
||||
val itemName = item.name.lowercase(Locale.getDefault())
|
||||
itemName.contains(query.lowercase(Locale.getDefault()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.entriesScreen
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.rtbishop.look4sat.databinding.ItemModeBinding
|
||||
|
||||
class ModesAdapter(private val clickListener: ModesClickListener) :
|
||||
RecyclerView.Adapter<ModesAdapter.ModeHolder>() {
|
||||
|
||||
private val diffCallback = object : DiffUtil.ItemCallback<String>() {
|
||||
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
private val differ = AsyncListDiffer(this, diffCallback)
|
||||
private var selectedModes = listOf<String>()
|
||||
|
||||
fun submitModes(items: List<String>, selected: List<String>) {
|
||||
differ.submitList(items)
|
||||
selectedModes = selected
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = differ.currentList.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ModeHolder {
|
||||
return ModeHolder.from(parent)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ModeHolder, position: Int) {
|
||||
val currentItem = differ.currentList[position]
|
||||
holder.bind(currentItem, selectedModes.contains(currentItem), clickListener)
|
||||
}
|
||||
|
||||
class ModeHolder(private val binding: ItemModeBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(mode: String, isSelected: Boolean, listener: ModesClickListener) {
|
||||
binding.modeCheckbox.text = mode
|
||||
binding.modeCheckbox.isChecked = isSelected
|
||||
binding.modeCheckbox.setOnClickListener {
|
||||
listener.onModeClicked(mode, isSelected.not())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(parent: ViewGroup): ModeHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return ModeHolder(ItemModeBinding.inflate(inflater, parent, false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ModesClickListener {
|
||||
fun onModeClicked(mode: String, isSelected: Boolean)
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.entriesScreen
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.rtbishop.look4sat.R
|
||||
import com.rtbishop.look4sat.databinding.DialogModesBinding
|
||||
import com.rtbishop.look4sat.domain.ISettingsManager
|
||||
import com.rtbishop.look4sat.presentation.setNavResult
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ModesDialog : AppCompatDialogFragment(), ModesAdapter.ModesClickListener {
|
||||
|
||||
@Inject
|
||||
lateinit var preferences: ISettingsManager
|
||||
private lateinit var binding: DialogModesBinding
|
||||
private val allModes = listOf(
|
||||
"AFSK", "AFSK S-Net", "AFSK SALSAT", "AHRPT", "AM", "APT", "BPSK", "BPSK PMT-A3",
|
||||
"CERTO", "CW", "DQPSK", "DSTAR", "DUV", "FFSK", "FM", "FMN", "FSK", "FSK AX.100 Mode 5",
|
||||
"FSK AX.100 Mode 6", "FSK AX.25 G3RUH", "GFSK", "GFSK Rktr", "GMSK", "HRPT", "LoRa",
|
||||
"LRPT", "LSB", "MFSK", "MSK", "MSK AX.100 Mode 5", "MSK AX.100 Mode 6", "OFDM", "OQPSK",
|
||||
"PSK", "PSK31", "PSK63", "QPSK", "QPSK31", "QPSK63", "SSTV", "USB", "WSJT"
|
||||
)
|
||||
private val selectedModes = mutableListOf<String>()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, group: ViewGroup?, state: Bundle?): View {
|
||||
binding = DialogModesBinding.inflate(inflater, group, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, state: Bundle?) {
|
||||
super.onViewCreated(view, state)
|
||||
dialog?.window?.setBackgroundDrawableResource(R.color.transparent)
|
||||
dialog?.window?.setLayout(
|
||||
(resources.displayMetrics.widthPixels * 0.94).toInt(),
|
||||
(resources.displayMetrics.heightPixels * 0.76).toInt()
|
||||
)
|
||||
val modesAdapter = ModesAdapter(this@ModesDialog)
|
||||
val layoutManager = GridLayoutManager(context, 2)
|
||||
val itemDecoration = DividerItemDecoration(context, layoutManager.orientation)
|
||||
selectedModes.addAll(preferences.loadModesSelection())
|
||||
modesAdapter.submitModes(allModes, selectedModes)
|
||||
binding.run {
|
||||
modesRecycler.apply {
|
||||
setHasFixedSize(true)
|
||||
this.adapter = modesAdapter
|
||||
this.layoutManager = layoutManager
|
||||
addItemDecoration(itemDecoration)
|
||||
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
}
|
||||
modesBtnNeg.setOnClickListener { dismiss() }
|
||||
modesBtnPos.setOnClickListener {
|
||||
setNavResult("modes", selectedModes.toList())
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onModeClicked(mode: String, isSelected: Boolean) {
|
||||
when {
|
||||
isSelected -> selectedModes.add(mode)
|
||||
selectedModes.contains(mode) -> selectedModes.remove(mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.mapScreen
|
||||
|
||||
import com.rtbishop.look4sat.domain.predict.GeoPos
|
||||
|
||||
data class MapData(
|
||||
val catNum: Int,
|
||||
val name: String,
|
||||
val aosTime: String,
|
||||
val azimuth: Double,
|
||||
val elevation: Double,
|
||||
val range: Double,
|
||||
val altitude: Double,
|
||||
val velocity: Double,
|
||||
val qthLoc: String,
|
||||
val osmPos: GeoPos,
|
||||
val period: Double,
|
||||
val phase: Double,
|
||||
val eclipsed: Boolean
|
||||
)
|
||||
@@ -1,240 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.mapScreen
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import com.rtbishop.look4sat.R
|
||||
import com.rtbishop.look4sat.databinding.FragmentMapBinding
|
||||
import com.rtbishop.look4sat.domain.predict.GeoPos
|
||||
import com.rtbishop.look4sat.domain.predict.SatPos
|
||||
import com.rtbishop.look4sat.domain.predict.Satellite
|
||||
import com.rtbishop.look4sat.presentation.clickWithDebounce
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.osmdroid.config.Configuration
|
||||
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
|
||||
import org.osmdroid.util.GeoPoint
|
||||
import org.osmdroid.views.CustomZoomButtonsController
|
||||
import org.osmdroid.views.MapView
|
||||
import org.osmdroid.views.overlay.FolderOverlay
|
||||
import org.osmdroid.views.overlay.Marker
|
||||
import org.osmdroid.views.overlay.Polyline
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MapFragment : Fragment(R.layout.fragment_map) {
|
||||
|
||||
private val viewModel: MapViewModel by viewModels()
|
||||
private val navArgs: MapFragmentArgs by navArgs()
|
||||
private val minLat = MapView.getTileSystem().minLatitude
|
||||
private val maxLat = MapView.getTileSystem().maxLatitude
|
||||
private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
strokeWidth = 3f
|
||||
style = Paint.Style.STROKE
|
||||
color = Color.RED
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
}
|
||||
private val footprintPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
strokeWidth = 3f
|
||||
style = Paint.Style.FILL_AND_STROKE
|
||||
color = Color.parseColor("#FFE082")
|
||||
}
|
||||
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
textSize = 36f
|
||||
style = Paint.Style.FILL
|
||||
color = Color.parseColor("#CCFFFFFF")
|
||||
setShadowLayer(3f, 3f, 3f, Color.BLACK)
|
||||
}
|
||||
private val labelRect = Rect()
|
||||
private lateinit var binding: FragmentMapBinding
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding = FragmentMapBinding.bind(view)
|
||||
setupComponents()
|
||||
setupObservers()
|
||||
}
|
||||
|
||||
private fun setupComponents() {
|
||||
val context = requireContext()
|
||||
val preferences = context.getSharedPreferences("default", Context.MODE_PRIVATE)
|
||||
Configuration.getInstance().load(context, preferences)
|
||||
binding.run {
|
||||
mapView.apply {
|
||||
setMultiTouchControls(true)
|
||||
setTileSource(TileSourceFactory.WIKIMEDIA)
|
||||
minZoomLevel = getMinZoom(resources.displayMetrics.heightPixels) + 0.25
|
||||
maxZoomLevel = 5.75
|
||||
controller.setZoom(minZoomLevel + 0.25)
|
||||
zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
|
||||
overlayManager.tilesOverlay.loadingBackgroundColor = Color.TRANSPARENT
|
||||
overlayManager.tilesOverlay.loadingLineColor = Color.TRANSPARENT
|
||||
overlayManager.tilesOverlay.setColorFilter(getColorFilter())
|
||||
setScrollableAreaLimitLatitude(maxLat, minLat, 0)
|
||||
// add overlays: 0 - GSP, 1 - SatTrack, 2 - SatFootprint, 3 - SatIcons
|
||||
overlays.addAll(Array(4) { FolderOverlay() })
|
||||
}
|
||||
mapBtnBack.clickWithDebounce { findNavController().navigateUp() }
|
||||
mapBtnPrev.setOnClickListener { viewModel.scrollSelection(true) }
|
||||
mapBtnNext.setOnClickListener { viewModel.scrollSelection(false) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupObservers() {
|
||||
viewModel.selectDefaultSatellite(navArgs.catNum)
|
||||
viewModel.stationPos.observe(viewLifecycleOwner) { handleStationPos(it) }
|
||||
viewModel.positions.observe(viewLifecycleOwner) { handlePositions(it) }
|
||||
viewModel.track.observe(viewLifecycleOwner) { handleTrack(it) }
|
||||
viewModel.footprint.observe(viewLifecycleOwner) { handleFootprint(it) }
|
||||
viewModel.mapData.observe(viewLifecycleOwner) { handleMapData(it) }
|
||||
}
|
||||
|
||||
private fun handleStationPos(stationPos: GeoPos) {
|
||||
binding.apply {
|
||||
Marker(mapView).apply {
|
||||
setInfoWindow(null)
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||
icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_position)
|
||||
position = GeoPoint(stationPos.lat, stationPos.lon)
|
||||
mapView.overlays[0] = this
|
||||
mapView.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePositions(posMap: Map<Satellite, GeoPos>) {
|
||||
binding.apply {
|
||||
val markers = FolderOverlay()
|
||||
posMap.entries.forEach {
|
||||
Marker(mapView).apply {
|
||||
setInfoWindow(null)
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
|
||||
icon = getCustomTextIcon(it.key.data.name)
|
||||
try {
|
||||
position = GeoPoint(it.value.lat, it.value.lon)
|
||||
} catch (exception: IllegalArgumentException) {
|
||||
println(exception.stackTraceToString())
|
||||
}
|
||||
setOnMarkerClickListener { _, _ ->
|
||||
viewModel.selectSatellite(it.key)
|
||||
return@setOnMarkerClickListener true
|
||||
}
|
||||
markers.add(this)
|
||||
}
|
||||
}
|
||||
mapView.overlays[3] = markers
|
||||
mapView.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCustomTextIcon(textLabel: String): Drawable {
|
||||
textPaint.getTextBounds(textLabel, 0, textLabel.length, labelRect)
|
||||
val iconSize = 8f
|
||||
val width = labelRect.width() + iconSize * 2f
|
||||
val height = textPaint.textSize * 3f + iconSize * 2f
|
||||
val bitmap = Bitmap.createBitmap(width.toInt(), height.toInt(), Bitmap.Config.ARGB_8888)
|
||||
Canvas(bitmap).run {
|
||||
drawCircle(width / 2f, height / 2f, iconSize, textPaint)
|
||||
drawText(textLabel, iconSize / 2f, height - iconSize, textPaint)
|
||||
}
|
||||
return BitmapDrawable(binding.mapView.context.resources, bitmap)
|
||||
}
|
||||
|
||||
private fun handleTrack(satTrack: List<List<GeoPos>>) {
|
||||
val center = GeoPoint(satTrack[0][0].lat, satTrack[0][0].lon)
|
||||
val trackOverlay = FolderOverlay()
|
||||
satTrack.forEach { track ->
|
||||
val trackPoints = track.map { GeoPoint(it.lat, it.lon) }
|
||||
Polyline().apply {
|
||||
try {
|
||||
setPoints(trackPoints)
|
||||
outlinePaint.set(trackPaint)
|
||||
trackOverlay.add(this)
|
||||
} catch (exception: IllegalArgumentException) {
|
||||
println(exception.stackTraceToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.mapView.overlays[1] = trackOverlay
|
||||
binding.mapView.controller.animateTo(center)
|
||||
}
|
||||
|
||||
private fun handleFootprint(satPos: SatPos) {
|
||||
val footprintPoints = satPos.getRangeCircle().map { GeoPoint(it.lat, it.lon) }
|
||||
val footprintOverlay = Polyline().apply {
|
||||
outlinePaint.set(footprintPaint)
|
||||
try {
|
||||
setPoints(footprintPoints)
|
||||
} catch (exception: IllegalArgumentException) {
|
||||
println(exception.stackTraceToString())
|
||||
}
|
||||
}
|
||||
binding.mapView.overlays[2] = footprintOverlay
|
||||
}
|
||||
|
||||
private fun handleMapData(mapData: MapData) {
|
||||
binding.apply {
|
||||
mapTimer.text = mapData.aosTime
|
||||
mapDataPeriod.text = String.format(getString(R.string.map_period, mapData.period))
|
||||
mapDataPhase.text = String.format(getString(R.string.map_phase), mapData.phase)
|
||||
mapAzimuth.text = String.format(getString(R.string.map_azimuth), mapData.azimuth)
|
||||
mapElevation.text = String.format(getString(R.string.map_elevation), mapData.elevation)
|
||||
mapDataAlt.text = String.format(getString(R.string.map_altitude), mapData.altitude)
|
||||
mapDataDst.text = String.format(getString(R.string.map_distance), mapData.range)
|
||||
mapDataLat.text =
|
||||
String.format(getString(R.string.map_latitude), mapData.osmPos.lat)
|
||||
mapDataLon.text =
|
||||
String.format(getString(R.string.map_longitude), mapData.osmPos.lon)
|
||||
mapDataQth.text = String.format(getString(R.string.map_qth), mapData.qthLoc)
|
||||
mapDataVel.text = String.format(getString(R.string.map_velocity), mapData.velocity)
|
||||
if (mapData.eclipsed) {
|
||||
mapDataVisibility.text = getString(R.string.map_eclipsed)
|
||||
} else {
|
||||
mapDataVisibility.text = getString(R.string.map_visible)
|
||||
}
|
||||
}
|
||||
binding.mapView.invalidate()
|
||||
}
|
||||
|
||||
private fun getColorFilter(): ColorMatrixColorFilter {
|
||||
val grayScaleMatrix = ColorMatrix().apply { setSaturation(0f) }
|
||||
val negativeMatrix = ColorMatrix(
|
||||
floatArrayOf(
|
||||
-1f, 0f, 0f, 0f, 255f,
|
||||
0f, -1f, 0f, 0f, 255f,
|
||||
0f, 0f, -1f, 0f, 255f,
|
||||
0f, 0f, 0f, 1f, 0f
|
||||
)
|
||||
)
|
||||
negativeMatrix.preConcat(grayScaleMatrix)
|
||||
return ColorMatrixColorFilter(negativeMatrix)
|
||||
}
|
||||
|
||||
private fun getMinZoom(screenHeight: Int): Double {
|
||||
return MapView.getTileSystem().getLatitudeZoom(maxLat, minLat, screenHeight)
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.mapScreen
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import com.rtbishop.look4sat.domain.IDataRepository
|
||||
import com.rtbishop.look4sat.domain.ISatelliteManager
|
||||
import com.rtbishop.look4sat.domain.ISettingsManager
|
||||
import com.rtbishop.look4sat.domain.predict.GeoPos
|
||||
import com.rtbishop.look4sat.domain.predict.SatPos
|
||||
import com.rtbishop.look4sat.domain.predict.Satellite
|
||||
import com.rtbishop.look4sat.utility.QthConverter
|
||||
import com.rtbishop.look4sat.utility.toDegrees
|
||||
import com.rtbishop.look4sat.utility.toTimerString
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
@HiltViewModel
|
||||
class MapViewModel @Inject constructor(
|
||||
private val satelliteManager: ISatelliteManager,
|
||||
private val repository: IDataRepository,
|
||||
private val settings: ISettingsManager,
|
||||
) : ViewModel() {
|
||||
|
||||
private val stationPosition = settings.loadStationPosition()
|
||||
private var allPasses = satelliteManager.getPasses()
|
||||
private var allSatellites = listOf<Satellite>()
|
||||
private var dataUpdateJob: Job? = null
|
||||
private var dataUpdateRate = 1000L
|
||||
private var selectedSatellite: Satellite? = null
|
||||
|
||||
val stationPos = liveData {
|
||||
val osmLat = clipLat(stationPosition.lat)
|
||||
val osmLon = clipLon(stationPosition.lon)
|
||||
emit(GeoPos(osmLat, osmLon))
|
||||
}
|
||||
|
||||
private val _track = MutableLiveData<List<List<GeoPos>>>()
|
||||
val track: LiveData<List<List<GeoPos>>> = _track
|
||||
|
||||
private val _footprint = MutableLiveData<SatPos>()
|
||||
val footprint: LiveData<SatPos> = _footprint
|
||||
|
||||
private val _mapData = MutableLiveData<MapData>()
|
||||
val mapData: LiveData<MapData> = this._mapData
|
||||
|
||||
private val _positions = MutableLiveData<Map<Satellite, GeoPos>>()
|
||||
val positions: LiveData<Map<Satellite, GeoPos>> = _positions
|
||||
|
||||
fun scrollSelection(decrement: Boolean) {
|
||||
if (allSatellites.isNotEmpty()) {
|
||||
val index = allSatellites.indexOf(selectedSatellite)
|
||||
if (decrement) {
|
||||
if (index > 0) selectSatellite(allSatellites[index - 1])
|
||||
else selectSatellite(allSatellites[allSatellites.size - 1])
|
||||
} else {
|
||||
if (index < allSatellites.size - 1) selectSatellite(allSatellites[index + 1])
|
||||
else selectSatellite(allSatellites[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selectDefaultSatellite(catnum: Int) {
|
||||
viewModelScope.launch {
|
||||
val selectedIds = settings.loadEntriesSelection()
|
||||
repository.getEntriesWithIds(selectedIds).also { satellites ->
|
||||
if (satellites.isNotEmpty()) {
|
||||
allSatellites = satellites
|
||||
if (catnum == -1) {
|
||||
allPasses.find { pass -> pass.progress < 100 && !pass.isDeepSpace }
|
||||
?.let { pass -> selectSatellite(pass.satellite) }
|
||||
} else {
|
||||
satellites.find { it.data.catnum == catnum }?.let { selectSatellite(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selectSatellite(satellite: Satellite, updateFreq: Long = dataUpdateRate) {
|
||||
selectedSatellite = satellite
|
||||
viewModelScope.launch {
|
||||
dataUpdateJob?.cancelAndJoin()
|
||||
dataUpdateJob = launch {
|
||||
val dateNow = Date()
|
||||
getSatTrack(satellite, stationPosition, dateNow)
|
||||
while (isActive) {
|
||||
dateNow.time = System.currentTimeMillis()
|
||||
getPositions(allSatellites, stationPosition, dateNow)
|
||||
getSatFootprint(satellite, stationPosition, dateNow)
|
||||
getSatData(satellite, stationPosition, dateNow)
|
||||
delay(updateFreq)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getSatTrack(satellite: Satellite, pos: GeoPos, date: Date) {
|
||||
val satTracks = mutableListOf<List<GeoPos>>()
|
||||
val currentTrack = mutableListOf<GeoPos>()
|
||||
val endDate = Date(date.time + (satellite.data.orbitalPeriod * 2.4 * 60000L).toLong())
|
||||
var oldLongitude = 0.0
|
||||
satelliteManager.getTrack(satellite, pos, date.time, endDate.time).forEach { satPos ->
|
||||
val osmLat = clipLat(satPos.latitude.toDegrees())
|
||||
val osmLon = clipLon(satPos.longitude.toDegrees())
|
||||
val currentPosition = GeoPos(osmLat, osmLon)
|
||||
if (oldLongitude < -170.0 && currentPosition.lon > 170.0) {
|
||||
// adding left terminal position
|
||||
currentTrack.add(GeoPos(osmLat, -180.0))
|
||||
val finishedTrack = mutableListOf<GeoPos>().apply { addAll(currentTrack) }
|
||||
satTracks.add(finishedTrack)
|
||||
currentTrack.clear()
|
||||
} else if (oldLongitude > 170.0 && currentPosition.lon < -170.0) {
|
||||
// adding right terminal position
|
||||
currentTrack.add(GeoPos(osmLat, 180.0))
|
||||
val finishedTrack = mutableListOf<GeoPos>().apply { addAll(currentTrack) }
|
||||
satTracks.add(finishedTrack)
|
||||
currentTrack.clear()
|
||||
}
|
||||
oldLongitude = currentPosition.lon
|
||||
currentTrack.add(currentPosition)
|
||||
}
|
||||
satTracks.add(currentTrack)
|
||||
_track.postValue(satTracks)
|
||||
}
|
||||
|
||||
private suspend fun getPositions(satellites: List<Satellite>, pos: GeoPos, date: Date) {
|
||||
val positions = mutableMapOf<Satellite, GeoPos>()
|
||||
satellites.forEach { satellite ->
|
||||
val satPos = satelliteManager.getPosition(satellite, pos, date.time)
|
||||
val osmLat = clipLat(satPos.latitude.toDegrees())
|
||||
val osmLon = clipLon(satPos.longitude.toDegrees())
|
||||
positions[satellite] = GeoPos(osmLat, osmLon)
|
||||
}
|
||||
_positions.postValue(positions)
|
||||
}
|
||||
|
||||
private suspend fun getSatFootprint(satellite: Satellite, pos: GeoPos, date: Date) {
|
||||
val satPos = satelliteManager.getPosition(satellite, pos, date.time)
|
||||
_footprint.postValue(satPos)
|
||||
}
|
||||
|
||||
private suspend fun getSatData(sat: Satellite, pos: GeoPos, date: Date) {
|
||||
var aosTime = 0L.toTimerString()
|
||||
allPasses.find { pass -> pass.catNum == sat.data.catnum && pass.progress < 100 }
|
||||
?.let { satPass ->
|
||||
if (!satPass.isDeepSpace) {
|
||||
aosTime = if (date.time < satPass.aosTime) {
|
||||
val millisBeforeStart = satPass.aosTime.minus(date.time)
|
||||
millisBeforeStart.toTimerString()
|
||||
} else {
|
||||
val millisBeforeEnd = satPass.losTime.minus(date.time)
|
||||
millisBeforeEnd.toTimerString()
|
||||
}
|
||||
}
|
||||
}
|
||||
val satPos = satelliteManager.getPosition(sat, pos, date.time)
|
||||
val azimuth = satPos.azimuth.toDegrees()
|
||||
val elevation = satPos.elevation.toDegrees()
|
||||
val osmLat = clipLat(satPos.latitude.toDegrees())
|
||||
val osmLon = clipLon(satPos.longitude.toDegrees())
|
||||
val osmPos = GeoPos(osmLat, osmLon)
|
||||
val qthLoc = QthConverter.positionToQth(osmPos.lat, osmPos.lon) ?: "-- --"
|
||||
val velocity = satPos.getOrbitalVelocity()
|
||||
val phase = satPos.phase.toDegrees()
|
||||
val visibility = satPos.eclipsed
|
||||
val satData = MapData(
|
||||
sat.data.catnum, sat.data.name, aosTime, azimuth, elevation, satPos.distance,
|
||||
satPos.altitude, velocity, qthLoc, osmPos, sat.data.orbitalPeriod, phase, visibility
|
||||
)
|
||||
_mapData.postValue(satData)
|
||||
}
|
||||
|
||||
private fun clipLat(latitude: Double): Double {
|
||||
return clip(latitude, -85.05, 85.05)
|
||||
}
|
||||
|
||||
private fun clipLon(longitude: Double): Double {
|
||||
var result = longitude
|
||||
while (result < -180.0) result += 360.0
|
||||
while (result > 180.0) result -= 360.0
|
||||
return clip(result, -180.0, 180.0)
|
||||
}
|
||||
|
||||
private fun clip(currentValue: Double, minValue: Double, maxValue: Double): Double {
|
||||
return min(max(currentValue, minValue), maxValue)
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.passesScreen
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import com.rtbishop.look4sat.R
|
||||
import com.rtbishop.look4sat.databinding.DialogFilterBinding
|
||||
import com.rtbishop.look4sat.domain.ISettingsManager
|
||||
import com.rtbishop.look4sat.presentation.setNavResult
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class FilterDialog : AppCompatDialogFragment() {
|
||||
|
||||
@Inject
|
||||
lateinit var preferences: ISettingsManager
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, group: ViewGroup?, state: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.dialog_filter, group, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, state: Bundle?) {
|
||||
super.onViewCreated(view, state)
|
||||
DialogFilterBinding.bind(view).apply {
|
||||
dialog?.window?.setBackgroundDrawableResource(R.color.transparent)
|
||||
dialog?.window?.setLayout(
|
||||
(resources.displayMetrics.widthPixels * 0.94).toInt(),
|
||||
WindowManager.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
filterHoursEdit.setText(preferences.getHoursAhead().toString())
|
||||
filterElevEdit.setText(preferences.getMinElevation().toString())
|
||||
filterBtnPos.setOnClickListener {
|
||||
val hoursAhead = try {
|
||||
filterHoursEdit.text.toString().toInt()
|
||||
} catch (exception: Exception) {
|
||||
24
|
||||
}
|
||||
val minElevation = try {
|
||||
filterElevEdit.text.toString().toDouble()
|
||||
} catch (exception: Exception) {
|
||||
16.0
|
||||
}
|
||||
setNavResult("prefs", Pair(hoursAhead, minElevation))
|
||||
dismiss()
|
||||
}
|
||||
filterBtnNeg.setOnClickListener { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.passesScreen
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.rtbishop.look4sat.R
|
||||
import com.rtbishop.look4sat.databinding.ItemPassBinding
|
||||
import com.rtbishop.look4sat.domain.predict.SatPass
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class PassesAdapter(private val clickListener: PassesClickListener) :
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private val diffCallback = object : DiffUtil.ItemCallback<SatPass>() {
|
||||
override fun areItemsTheSame(oldItem: SatPass, newItem: SatPass): Boolean {
|
||||
return oldItem.catNum == newItem.catNum && oldItem.aosTime == newItem.aosTime
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: SatPass, newItem: SatPass): Boolean {
|
||||
return oldItem.progress == newItem.progress
|
||||
}
|
||||
}
|
||||
private val differ = AsyncListDiffer(this, diffCallback)
|
||||
private var isUTC: Boolean = false
|
||||
|
||||
interface PassesClickListener {
|
||||
fun navigateToPass(satPass: SatPass)
|
||||
}
|
||||
|
||||
fun submitList(passes: List<SatPass>) {
|
||||
differ.submitList(passes)
|
||||
}
|
||||
|
||||
fun setUTC(isUTC: Boolean) {
|
||||
this.isUTC = isUTC
|
||||
}
|
||||
|
||||
override fun getItemCount() = differ.currentList.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return PassHolder.from(parent)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
(holder as PassHolder).bind(differ.currentList[position], clickListener, isUTC)
|
||||
}
|
||||
|
||||
class PassHolder private constructor(private val binding: ItemPassBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
private val satIdFormat = itemView.context.getString(R.string.pass_satId)
|
||||
private val aosAzFormat = itemView.context.getString(R.string.pass_aosAz)
|
||||
private val altFormat = itemView.context.getString(R.string.pass_altitude)
|
||||
private val losAzFormat = itemView.context.getString(R.string.pass_los)
|
||||
private val startTimeFormat = itemView.context.getString(R.string.pass_startTime)
|
||||
private val elevFormat = itemView.context.getString(R.string.pass_elevation)
|
||||
private val endTimeFormat = itemView.context.getString(R.string.pass_endTime)
|
||||
private val placeholder = itemView.context.getString(R.string.pass_placeholder)
|
||||
private val timeZoneUTC = TimeZone.getTimeZone("UTC")
|
||||
private val startFormat = SimpleDateFormat(startTimeFormat, Locale.getDefault())
|
||||
private val endFormat = SimpleDateFormat(endTimeFormat, Locale.getDefault())
|
||||
|
||||
fun bind(satPass: SatPass, listener: PassesClickListener, shouldUseUTC: Boolean) {
|
||||
binding.apply {
|
||||
if (satPass.isDeepSpace) {
|
||||
passName.text = satPass.name
|
||||
passId.text = String.format(satIdFormat, satPass.catNum)
|
||||
passAos.text = String.format(aosAzFormat, satPass.aosAzimuth)
|
||||
passAltitude.text = String.format(altFormat, satPass.altitude)
|
||||
passLos.text = String.format(losAzFormat, satPass.losAzimuth)
|
||||
passStart.text = placeholder
|
||||
passElev.text = String.format(elevFormat, satPass.maxElevation)
|
||||
passEnd.text = placeholder
|
||||
passProgress.progress = 100
|
||||
} else {
|
||||
if (shouldUseUTC) {
|
||||
startFormat.timeZone = timeZoneUTC
|
||||
endFormat.timeZone = timeZoneUTC
|
||||
}
|
||||
passName.text = satPass.name
|
||||
passId.text = String.format(satIdFormat, satPass.catNum)
|
||||
passAos.text = String.format(aosAzFormat, satPass.aosAzimuth)
|
||||
passAltitude.text = String.format(altFormat, satPass.altitude)
|
||||
passLos.text = String.format(losAzFormat, satPass.losAzimuth)
|
||||
passStart.text = startFormat.format(Date(satPass.aosTime))
|
||||
passElev.text = String.format(elevFormat, satPass.maxElevation)
|
||||
passEnd.text = endFormat.format(Date(satPass.losTime))
|
||||
passProgress.progress = satPass.progress
|
||||
}
|
||||
}
|
||||
|
||||
itemView.setOnClickListener {
|
||||
listener.navigateToPass(satPass)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(parent: ViewGroup): PassHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return PassHolder(ItemPassBinding.inflate(inflater, parent, false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.passesScreen
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.rtbishop.look4sat.R
|
||||
import com.rtbishop.look4sat.databinding.FragmentPassesBinding
|
||||
import com.rtbishop.look4sat.domain.model.DataState
|
||||
import com.rtbishop.look4sat.domain.predict.SatPass
|
||||
import com.rtbishop.look4sat.presentation.clickWithDebounce
|
||||
import com.rtbishop.look4sat.presentation.getNavResult
|
||||
import com.rtbishop.look4sat.utility.toTimerString
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PassesFragment : Fragment(R.layout.fragment_passes), PassesAdapter.PassesClickListener {
|
||||
|
||||
private val viewModel: PassesViewModel by viewModels()
|
||||
private val passesAdapter = PassesAdapter(this)
|
||||
private var binding: FragmentPassesBinding? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding = FragmentPassesBinding.bind(view).apply {
|
||||
passesAdapter.setUTC(viewModel.shouldUseUTC())
|
||||
passesRecycler.apply {
|
||||
setHasFixedSize(true)
|
||||
adapter = passesAdapter
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
addItemDecoration(DividerItemDecoration(requireContext(), 1))
|
||||
addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
if (dy > 0 && passesFab.visibility == View.VISIBLE) passesFab.hide()
|
||||
else if (dy < 0 && passesFab.visibility != View.VISIBLE) passesFab.show()
|
||||
}
|
||||
})
|
||||
}
|
||||
passesBtnRefresh.clickWithDebounce { viewModel.calculatePasses() }
|
||||
passesBtnMap.clickWithDebounce {
|
||||
val dir = PassesFragmentDirections.globalToMap()
|
||||
findNavController().navigate(dir)
|
||||
}
|
||||
passesBtnFilter.clickWithDebounce {
|
||||
val dir = PassesFragmentDirections.passesToFilter()
|
||||
findNavController().navigate(dir)
|
||||
}
|
||||
passesBtnSettings.clickWithDebounce {
|
||||
val dir = PassesFragmentDirections.globalToSettings()
|
||||
findNavController().navigate(dir)
|
||||
}
|
||||
setupObservers()
|
||||
}
|
||||
}
|
||||
|
||||
override fun navigateToPass(satPass: SatPass) {
|
||||
if (satPass.progress < 100) satPass.run {
|
||||
val dir = PassesFragmentDirections.globalToRadar(this.catNum, this.aosTime)
|
||||
findNavController().navigate(dir)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding?.passesRecycler?.adapter = null
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setupObservers() {
|
||||
viewModel.entriesTotal.observe(viewLifecycleOwner) { number ->
|
||||
handleEntriesTotal(number)
|
||||
}
|
||||
viewModel.passes.observe(viewLifecycleOwner) { passesResult ->
|
||||
handleNewPasses(passesResult)
|
||||
}
|
||||
getNavResult<Pair<Int, Double>>(R.id.nav_passes, "prefs") { prefs ->
|
||||
viewModel.calculatePasses(prefs.first, prefs.second)
|
||||
}
|
||||
getNavResult<List<Int>>(R.id.nav_passes, "selection") { items ->
|
||||
viewModel.calculatePasses(selection = items)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEntriesTotal(number: Int) {
|
||||
binding?.run {
|
||||
if (number > 0) {
|
||||
passesFab.clickWithDebounce {
|
||||
val direction = PassesFragmentDirections.globalToEntries()
|
||||
findNavController().navigate(direction)
|
||||
}
|
||||
} else {
|
||||
passesFab.clickWithDebounce {
|
||||
val errorMessage = getString(R.string.passes_empty_db)
|
||||
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNewPasses(state: DataState<List<SatPass>>) {
|
||||
binding?.run {
|
||||
when (state) {
|
||||
is DataState.Success -> {
|
||||
passesAdapter.submitList(state.data)
|
||||
tickMainTimer(state.data)
|
||||
if (state.data.isNotEmpty()) { // show new passes list
|
||||
passesEmpty.visibility = View.INVISIBLE
|
||||
passesProgress.visibility = View.INVISIBLE
|
||||
} else { // show no passes message
|
||||
passesEmpty.visibility = View.VISIBLE
|
||||
passesProgress.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
is DataState.Loading -> {
|
||||
passesEmpty.visibility = View.INVISIBLE
|
||||
passesProgress.visibility = View.VISIBLE
|
||||
passesTimer.text = 0L.toTimerString()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun tickMainTimer(passes: List<SatPass>) {
|
||||
binding?.run {
|
||||
if (passes.isNotEmpty()) {
|
||||
val timeNow = System.currentTimeMillis()
|
||||
try {
|
||||
val nextPass = passes.first { it.aosTime.minus(timeNow) > 0 }
|
||||
val millisBeforeStart = nextPass.aosTime.minus(timeNow)
|
||||
passesTimer.text = millisBeforeStart.toTimerString()
|
||||
} catch (e: NoSuchElementException) {
|
||||
val lastPass = passes.last()
|
||||
val millisBeforeEnd = lastPass.losTime.minus(timeNow)
|
||||
passesTimer.text = millisBeforeEnd.toTimerString()
|
||||
}
|
||||
} else {
|
||||
passesTimer.text = 0L.toTimerString()
|
||||
}
|
||||
passesBtnRefresh.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.passesScreen
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import com.rtbishop.look4sat.domain.IDataRepository
|
||||
import com.rtbishop.look4sat.domain.ISatelliteManager
|
||||
import com.rtbishop.look4sat.domain.ISettingsManager
|
||||
import com.rtbishop.look4sat.domain.model.DataState
|
||||
import com.rtbishop.look4sat.domain.predict.SatPass
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class PassesViewModel @Inject constructor(
|
||||
private val satelliteManager: ISatelliteManager,
|
||||
private val repository: IDataRepository,
|
||||
private val settings: ISettingsManager
|
||||
) : ViewModel() {
|
||||
|
||||
private var passesProcessing: Job? = null
|
||||
private val _passes = MutableLiveData<DataState<List<SatPass>>>(DataState.Loading)
|
||||
val passes: LiveData<DataState<List<SatPass>>> = _passes
|
||||
val entriesTotal: LiveData<Int> = repository.getEntriesTotal().asLiveData()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
satelliteManager.calculatedPasses.collect { passes ->
|
||||
passesProcessing?.cancelAndJoin()
|
||||
passesProcessing = viewModelScope.launch {
|
||||
while (isActive) {
|
||||
val time = System.currentTimeMillis()
|
||||
val newPasses = satelliteManager.processPasses(passes, time)
|
||||
_passes.postValue(DataState.Success(newPasses))
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
repository.updateState.collect { updateState ->
|
||||
if (updateState is DataState.Success) calculatePasses()
|
||||
}
|
||||
}
|
||||
calculatePasses()
|
||||
}
|
||||
|
||||
fun shouldUseUTC() = settings.getUseUTC()
|
||||
|
||||
fun calculatePasses(
|
||||
hoursAhead: Int = settings.getHoursAhead(),
|
||||
minElevation: Double = settings.getMinElevation(),
|
||||
timeRef: Long = System.currentTimeMillis(),
|
||||
selection: List<Int>? = null
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
_passes.postValue(DataState.Loading)
|
||||
passesProcessing?.cancelAndJoin()
|
||||
selection?.let { items -> settings.saveEntriesSelection(items) }
|
||||
settings.setHoursAhead(hoursAhead)
|
||||
settings.setMinElevation(minElevation)
|
||||
val stationPos = settings.loadStationPosition()
|
||||
val selectedIds = settings.loadEntriesSelection()
|
||||
val satellites = repository.getEntriesWithIds(selectedIds)
|
||||
satelliteManager.calculatePasses(satellites, stationPos, timeRef, hoursAhead, minElevation)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package com.rtbishop.look4sat.presentation.radarScreen
|
||||
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.OutputStream
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
|
||||
class BTReporter(private val manager: BluetoothManager, private val reporterScope: CoroutineScope) {
|
||||
|
||||
private val tag = "BTReporter"
|
||||
private val sppid: UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb")
|
||||
private lateinit var outputStream: OutputStream
|
||||
private var rotationConnectionJob: Job? = null
|
||||
private var rotationReportingJob: Job? = null
|
||||
private var connected = false
|
||||
private var connecting = false
|
||||
|
||||
fun isConnected(): Boolean = connected
|
||||
|
||||
fun isConnecting(): Boolean = connecting
|
||||
|
||||
fun connectBTDevice(deviceId: String) {
|
||||
if (!connected) {
|
||||
rotationConnectionJob = reporterScope.launch {
|
||||
try {
|
||||
manager.adapter.getRemoteDevice(deviceId)?.let { device ->
|
||||
device.createInsecureRfcommSocketToServiceRecord(sppid)?.let { socket ->
|
||||
connecting = true
|
||||
socket.connect()
|
||||
outputStream = socket.outputStream
|
||||
connected = true
|
||||
connecting = false
|
||||
Log.i(tag, "$tag: Connected!")
|
||||
}
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(tag, "$tag: ${e.message}")
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "$tag: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reportRotation(format: String, azimuth: Int, elevation: Int) {
|
||||
if (connected) {
|
||||
rotationReportingJob = reporterScope.launch {
|
||||
val newElevation = if (elevation > 0) elevation else 0
|
||||
try {
|
||||
val azimuthString = intToStringWithLeadingZeroes(azimuth)
|
||||
val elevationString = intToStringWithLeadingZeroes(newElevation)
|
||||
val crChar = '\r'
|
||||
val nlChar = '\n'
|
||||
val tbChar = '\t'
|
||||
var buffer = format.replace("\$AZ", azimuthString)
|
||||
buffer = buffer.replace("\$EL", elevationString)
|
||||
buffer = buffer.replace("\\r", crChar.toString())
|
||||
buffer = buffer.replace("\\n", nlChar.toString())
|
||||
buffer = buffer.replace("\\t", tbChar.toString())
|
||||
Log.i(tag, "$tag: Sending $buffer")
|
||||
if (connected) outputStream.write(buffer.toByteArray())
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "$tag: ${e.message}")
|
||||
connected = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun intToStringWithLeadingZeroes(value: Int): String {
|
||||
return if (value > 0) {
|
||||
if (value < 10) "00$value" else if (value < 100) "0$value" else "$value"
|
||||
} else {
|
||||
val absValue = abs(value)
|
||||
if (value > -10) "-00$absValue" else if (value > -100) "-0$absValue" else "-$absValue"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.radarScreen
|
||||
|
||||
import com.rtbishop.look4sat.domain.predict.SatPos
|
||||
|
||||
data class RadarData(val satPos: SatPos, val satTrack: List<SatPos> = emptyList())
|
||||
@@ -1,159 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.radarScreen
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.CalendarContract
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.rtbishop.look4sat.R
|
||||
import com.rtbishop.look4sat.databinding.FragmentRadarBinding
|
||||
import com.rtbishop.look4sat.domain.predict.SatPass
|
||||
import com.rtbishop.look4sat.domain.predict.SatPos
|
||||
import com.rtbishop.look4sat.presentation.clickWithDebounce
|
||||
import com.rtbishop.look4sat.utility.toDegrees
|
||||
import com.rtbishop.look4sat.utility.toTimerString
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RadarFragment : Fragment(R.layout.fragment_radar) {
|
||||
|
||||
private val viewModel: RadarViewModel by viewModels()
|
||||
private val navArgs: RadarFragmentArgs by navArgs()
|
||||
private val radioAdapter = RadioAdapter()
|
||||
private var binding: FragmentRadarBinding? = null
|
||||
private var radarView: RadarView? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding = FragmentRadarBinding.bind(view).apply {
|
||||
radarRecycler.apply {
|
||||
setHasFixedSize(true)
|
||||
this.adapter = radioAdapter
|
||||
this.layoutManager = LinearLayoutManager(requireContext())
|
||||
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
addItemDecoration(DividerItemDecoration(requireContext(), 1))
|
||||
}
|
||||
setupObservers()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.enableSensor()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
viewModel.disableSensor()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding?.radarRecycler?.adapter = null
|
||||
radarView = null
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setupObservers() {
|
||||
viewModel.getPass(navArgs.catNum, navArgs.aosTime).observe(viewLifecycleOwner) { pass ->
|
||||
binding?.run {
|
||||
radarView = RadarView(requireContext()).apply {
|
||||
setShowAim(viewModel.getUseCompass())
|
||||
setScanning(viewModel.getShowSweep())
|
||||
}
|
||||
radarCard.addView(radarView)
|
||||
viewModel.radarData.observe(viewLifecycleOwner) { passData ->
|
||||
radarView?.setPosition(passData.satPos)
|
||||
radarView?.setPositions(passData.satTrack)
|
||||
setPassText(pass, passData.satPos)
|
||||
}
|
||||
viewModel.transmitters.observe(viewLifecycleOwner) { list ->
|
||||
if (list.isNotEmpty()) {
|
||||
radioAdapter.submitList(list)
|
||||
radarProgress.visibility = View.INVISIBLE
|
||||
} else {
|
||||
radarProgress.visibility = View.INVISIBLE
|
||||
radarEmptyLayout.visibility = View.VISIBLE
|
||||
}
|
||||
radarView?.invalidate()
|
||||
}
|
||||
viewModel.orientation.observe(viewLifecycleOwner) { value ->
|
||||
radarView?.setOrientation(value.first, value.second, value.third)
|
||||
}
|
||||
radarBtnBack.clickWithDebounce { findNavController().navigateUp() }
|
||||
radarBtnMap.clickWithDebounce {
|
||||
val direction = RadarFragmentDirections.globalToMap(pass.catNum)
|
||||
findNavController().navigate(direction)
|
||||
}
|
||||
radarBtnNotify.clickWithDebounce {
|
||||
val intent = Intent(Intent.ACTION_INSERT)
|
||||
.setData(CalendarContract.Events.CONTENT_URI)
|
||||
.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, pass.aosTime)
|
||||
.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, pass.losTime)
|
||||
.putExtra(CalendarContract.Events.TITLE, pass.name)
|
||||
.putExtra(CalendarContract.Events.DESCRIPTION, "Look4Sat")
|
||||
startActivity(intent)
|
||||
}
|
||||
radarBtnSettings.clickWithDebounce {
|
||||
val direction = RadarFragmentDirections.globalToSettings()
|
||||
findNavController().navigate(direction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPassText(satPass: SatPass, satPos: SatPos) {
|
||||
binding?.run {
|
||||
val timeNow = System.currentTimeMillis()
|
||||
val radarAzim = getString(R.string.radar_az_value)
|
||||
val radarElev = getString(R.string.radar_el_value)
|
||||
val radarAlt = getString(R.string.radar_alt_value)
|
||||
val radarDist = getString(R.string.radar_dist_value)
|
||||
radarAzValue.text = String.format(radarAzim, satPos.azimuth.toDegrees())
|
||||
radarElValue.text = String.format(radarElev, satPos.elevation.toDegrees())
|
||||
radarAltValue.text = String.format(radarAlt, satPos.altitude)
|
||||
radarDstValue.text = String.format(radarDist, satPos.distance)
|
||||
if (satPos.eclipsed) {
|
||||
radarVisibility.text = getText(R.string.radar_eclipsed)
|
||||
} else {
|
||||
radarVisibility.text = getText(R.string.radar_visible)
|
||||
}
|
||||
if (!satPass.isDeepSpace) {
|
||||
if (timeNow < satPass.aosTime) {
|
||||
val millisBeforeStart = satPass.aosTime.minus(timeNow)
|
||||
radarTimer.text = millisBeforeStart.toTimerString()
|
||||
} else {
|
||||
val millisBeforeEnd = satPass.losTime.minus(timeNow)
|
||||
radarTimer.text = millisBeforeEnd.toTimerString()
|
||||
if (timeNow > satPass.losTime) {
|
||||
radarTimer.text = 0L.toTimerString()
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
} else radarTimer.text = 0L.toTimerString()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.radarScreen
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.rtbishop.look4sat.R
|
||||
import com.rtbishop.look4sat.domain.predict.PI_2
|
||||
import com.rtbishop.look4sat.domain.predict.SatPos
|
||||
import com.rtbishop.look4sat.domain.predict.TWO_PI
|
||||
import com.rtbishop.look4sat.utility.toRadians
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.min
|
||||
import kotlin.math.sin
|
||||
|
||||
class RadarView(context: Context) : View(context) {
|
||||
|
||||
private val defaultColor = ContextCompat.getColor(context, R.color.accent)
|
||||
private val scale = resources.displayMetrics.density
|
||||
private val strokeSize = scale * 2f
|
||||
private var position: SatPos? = null
|
||||
private var positions: List<SatPos> = emptyList()
|
||||
|
||||
private var radarColor = ContextCompat.getColor(context, R.color.textMain)
|
||||
private var radarCircleNum = 3
|
||||
private var radarPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = radarColor
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = strokeSize
|
||||
}
|
||||
|
||||
private var shouldShowSweep = true
|
||||
private var sweepColor = defaultColor
|
||||
private var sweepDegrees = 0f
|
||||
private var sweepSpeed = 15.0f
|
||||
private var sweepPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = sweepColor
|
||||
}
|
||||
|
||||
private var shouldShowBeacons = true
|
||||
private var beaconColor = defaultColor
|
||||
private var beaconSize = scale * 7f
|
||||
private var beaconPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = beaconColor
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
private var radarTextSize = scale * 16f
|
||||
private var radarTextColor = defaultColor
|
||||
private var radarTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = radarTextColor
|
||||
textSize = radarTextSize
|
||||
}
|
||||
|
||||
private var isTrackCreated = false
|
||||
private val trackPath: Path = Path()
|
||||
private var trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.RED
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = strokeSize
|
||||
}
|
||||
|
||||
private val arrowPath = Path()
|
||||
private var arrowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = ContextCompat.getColor(context, R.color.accent)
|
||||
style = Paint.Style.FILL
|
||||
strokeWidth = strokeSize
|
||||
}
|
||||
|
||||
private var shouldShowAim = true
|
||||
private var aimColor = Color.RED
|
||||
private var aimPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = aimColor
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = strokeSize
|
||||
}
|
||||
|
||||
private var shouldRotateRadar = true
|
||||
private var azimuth: Float = 0f
|
||||
private var pitch: Float = 0f
|
||||
private var roll: Float = 0f
|
||||
|
||||
fun setScanning(isScanning: Boolean) {
|
||||
shouldShowSweep = isScanning
|
||||
}
|
||||
|
||||
fun setPosition(position: SatPos) {
|
||||
this.position = position
|
||||
}
|
||||
|
||||
fun setPositions(positions: List<SatPos>) {
|
||||
this.positions = positions
|
||||
}
|
||||
|
||||
fun setShowAim(showAim: Boolean) {
|
||||
shouldShowAim = showAim
|
||||
}
|
||||
|
||||
fun setOrientation(azimuth: Float, pitch: Float, roll: Float) {
|
||||
this.azimuth = azimuth
|
||||
this.pitch = pitch
|
||||
this.roll = roll
|
||||
if (shouldRotateRadar) {
|
||||
this.rotation = -azimuth
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
val radarWidth = width - paddingLeft - paddingRight
|
||||
val radarHeight = height - paddingTop - paddingBottom
|
||||
val radarRadius = min(width, height) * 0.48f
|
||||
val cx = paddingLeft + radarWidth / 2f
|
||||
val cy = paddingTop + radarHeight / 2f
|
||||
|
||||
if (positions.isNotEmpty() && !isTrackCreated) {
|
||||
createPassTrajectory(radarRadius)
|
||||
createPassTrajectoryArrow()
|
||||
isTrackCreated = true
|
||||
}
|
||||
|
||||
canvas.drawColor(ContextCompat.getColor(context, R.color.cardRegular))
|
||||
drawRadarCircle(canvas, cx, cy, radarRadius)
|
||||
drawRadarCross(canvas, cx, cy, radarRadius)
|
||||
drawRadarText(canvas, cx, radarRadius)
|
||||
|
||||
canvas.translate(cx, cy)
|
||||
if (positions.isNotEmpty()) {
|
||||
canvas.drawPath(trackPath, trackPaint)
|
||||
canvas.drawPath(trackPath, arrowPaint)
|
||||
}
|
||||
if (shouldShowBeacons) {
|
||||
drawSatellite(canvas, radarRadius)
|
||||
}
|
||||
if (shouldShowAim) {
|
||||
drawCrosshair(canvas, azimuth, pitch, radarRadius)
|
||||
}
|
||||
if (shouldShowSweep) {
|
||||
canvas.translate(-cx, -cy)
|
||||
drawRadarSweep(canvas, cx, cy, radarRadius)
|
||||
sweepDegrees = (sweepDegrees + 360 / sweepSpeed / 60) % 360
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
|
||||
private fun drawRadarCircle(canvas: Canvas, cx: Float, cy: Float, radius: Float) {
|
||||
for (i in 0 until radarCircleNum) {
|
||||
canvas.drawCircle(cx, cy, radius - radius / radarCircleNum * i, radarPaint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawRadarCross(canvas: Canvas, cx: Float, cy: Float, radius: Float) {
|
||||
canvas.drawLine(cx - radius, cy, cx + radius, cy, radarPaint)
|
||||
canvas.drawLine(cx, cy - radius, cx, cy + radius, radarPaint)
|
||||
}
|
||||
|
||||
private fun drawRadarText(canvas: Canvas, cx: Float, radius: Float) {
|
||||
for (i in 0 until radarCircleNum) {
|
||||
val degText = " ${(90 / radarCircleNum) * (radarCircleNum - i)}°"
|
||||
canvas.drawText(degText, cx, radius - radius / radarCircleNum * i, radarTextPaint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawSatellite(canvas: Canvas, radarRadius: Float) {
|
||||
position?.let { satPos ->
|
||||
if (satPos.elevation > 0) {
|
||||
val satX = sph2CartX(satPos.azimuth, satPos.elevation, radarRadius.toDouble())
|
||||
val satY = sph2CartY(satPos.azimuth, satPos.elevation, radarRadius.toDouble())
|
||||
canvas.drawCircle(satX, -satY, beaconSize, beaconPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawCrosshair(canvas: Canvas, azimuth: Float, pitch: Float, radarRadius: Float) {
|
||||
val azimuthRad = azimuth.toDouble().toRadians()
|
||||
val tmpElevation = pitch.toDouble().toRadians()
|
||||
val elevationRad = if (tmpElevation > 0.0) 0.0 else tmpElevation
|
||||
val crossX = sph2CartX(azimuthRad, -elevationRad, radarRadius.toDouble())
|
||||
val crossY = sph2CartY(azimuthRad, -elevationRad, radarRadius.toDouble())
|
||||
canvas.drawLine(crossX - radarTextSize, -crossY, crossX + radarTextSize, -crossY, aimPaint)
|
||||
canvas.drawLine(crossX, -crossY - radarTextSize, crossX, -crossY + radarTextSize, aimPaint)
|
||||
canvas.drawCircle(crossX, -crossY, radarTextSize / 2, aimPaint)
|
||||
}
|
||||
|
||||
private fun sph2CartX(azimuth: Double, elevation: Double, r: Double): Float {
|
||||
val radius = r * (PI_2 - elevation) / PI_2
|
||||
return (radius * cos(PI_2 - azimuth)).toFloat()
|
||||
}
|
||||
|
||||
private fun sph2CartY(azimuth: Double, elevation: Double, r: Double): Float {
|
||||
val radius = r * (PI_2 - elevation) / PI_2
|
||||
return (radius * sin(PI_2 - azimuth)).toFloat()
|
||||
}
|
||||
|
||||
private fun createPassTrajectory(radarRadius: Float) {
|
||||
positions.forEachIndexed { index, satPos ->
|
||||
val passX = sph2CartX(satPos.azimuth, satPos.elevation, radarRadius.toDouble())
|
||||
val passY = sph2CartY(satPos.azimuth, satPos.elevation, radarRadius.toDouble())
|
||||
if (index == 0) {
|
||||
trackPath.moveTo(passX, -passY)
|
||||
} else {
|
||||
trackPath.lineTo(passX, -passY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPassTrajectoryArrow() {
|
||||
val radius = beaconSize
|
||||
val sides = 3
|
||||
val angle = TWO_PI / sides
|
||||
arrowPath.moveTo((radius * cos(angle)).toFloat(), (radius * sin(angle)).toFloat())
|
||||
for (i in 1 until sides) {
|
||||
val x = (radius * cos(angle - angle * i)).toFloat()
|
||||
val y = (radius * sin(angle - angle * i)).toFloat()
|
||||
arrowPath.lineTo(x, y)
|
||||
}
|
||||
arrowPath.close()
|
||||
val trackLength = PathMeasure(trackPath, false).length
|
||||
val quarter = trackLength / 4f
|
||||
val center = trackLength / 2f
|
||||
val effect = PathDashPathEffect(arrowPath, center, quarter, PathDashPathEffect.Style.ROTATE)
|
||||
arrowPaint.pathEffect = effect
|
||||
}
|
||||
|
||||
private fun drawRadarSweep(canvas: Canvas, cx: Float, cy: Float, radius: Float) {
|
||||
val sweepGradient = SweepGradient(
|
||||
cx,
|
||||
cy,
|
||||
intArrayOf(
|
||||
Color.TRANSPARENT,
|
||||
changeAlpha(sweepColor, 0),
|
||||
changeAlpha(sweepColor, 164),
|
||||
changeAlpha(sweepColor, 255),
|
||||
changeAlpha(sweepColor, 255)
|
||||
),
|
||||
floatArrayOf(0.0f, 0.55f, 0.996f, 0.999f, 1f)
|
||||
)
|
||||
sweepPaint.shader = sweepGradient
|
||||
canvas.rotate(-90 + sweepDegrees, cx, cy)
|
||||
canvas.drawCircle(cx, cy, radius, sweepPaint)
|
||||
}
|
||||
|
||||
private fun changeAlpha(color: Int, alpha: Int): Int {
|
||||
val red = Color.red(color)
|
||||
val green = Color.green(color)
|
||||
val blue = Color.blue(color)
|
||||
return Color.argb(alpha, red, green, blue)
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.radarScreen
|
||||
|
||||
import android.hardware.GeomagneticField
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.rtbishop.look4sat.domain.IDataRepository
|
||||
import com.rtbishop.look4sat.domain.ISatelliteManager
|
||||
import com.rtbishop.look4sat.domain.ISettingsManager
|
||||
import com.rtbishop.look4sat.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.domain.predict.GeoPos
|
||||
import com.rtbishop.look4sat.domain.predict.SatPass
|
||||
import com.rtbishop.look4sat.domain.predict.SatPos
|
||||
import com.rtbishop.look4sat.domain.predict.Satellite
|
||||
import com.rtbishop.look4sat.framework.OrientationManager
|
||||
import com.rtbishop.look4sat.utility.DataReporter
|
||||
import com.rtbishop.look4sat.utility.round
|
||||
import com.rtbishop.look4sat.utility.toDegrees
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@HiltViewModel
|
||||
class RadarViewModel @Inject constructor(
|
||||
private val orientationManager: OrientationManager,
|
||||
private val reporter: DataReporter,
|
||||
private val btReporter: BTReporter,
|
||||
private val satManager: ISatelliteManager,
|
||||
private val repository: IDataRepository,
|
||||
private val settings: ISettingsManager
|
||||
) : ViewModel(), OrientationManager.OrientationListener {
|
||||
|
||||
private val stationPos = settings.loadStationPosition()
|
||||
private val _passData = MutableLiveData<RadarData>()
|
||||
private val _transmitters = MutableLiveData<List<SatRadio>>()
|
||||
private val _orientation = MutableLiveData<Triple<Float, Float, Float>>()
|
||||
val radarData: LiveData<RadarData> = _passData
|
||||
val transmitters: LiveData<List<SatRadio>> = _transmitters
|
||||
val orientation: LiveData<Triple<Float, Float, Float>> = _orientation
|
||||
|
||||
fun getPass(catNum: Int, aosTime: Long) = liveData {
|
||||
satManager.calculatedPasses.collect { passes ->
|
||||
val pass = passes.find { pass -> pass.catNum == catNum && pass.aosTime == aosTime }
|
||||
pass?.let { satPass ->
|
||||
emit(satPass)
|
||||
val transmitters = repository.getRadiosWithId(satPass.catNum)
|
||||
viewModelScope.launch {
|
||||
while (isActive) {
|
||||
val timeNow = System.currentTimeMillis()
|
||||
val satPos = satManager.getPosition(satPass.satellite, stationPos, timeNow)
|
||||
sendPassData(satPass, satPos, satPass.satellite)
|
||||
sendPassDataBT(satPos)
|
||||
processRadios(transmitters, satPass.satellite, timeNow)
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun enableSensor() {
|
||||
if (settings.getUseCompass()) orientationManager.startListening(this)
|
||||
}
|
||||
|
||||
fun disableSensor() {
|
||||
if (settings.getUseCompass()) orientationManager.stopListening()
|
||||
}
|
||||
|
||||
fun getUseCompass(): Boolean = settings.getUseCompass()
|
||||
|
||||
fun getShowSweep(): Boolean = settings.getShowSweep()
|
||||
|
||||
override fun onOrientationChanged(azimuth: Float, pitch: Float, roll: Float) {
|
||||
_orientation.value = Triple(azimuth + getMagDeclination(stationPos), pitch, roll)
|
||||
}
|
||||
|
||||
private fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float {
|
||||
val latitude = geoPos.lat.toFloat()
|
||||
val longitude = geoPos.lon.toFloat()
|
||||
return GeomagneticField(latitude, longitude, 0f, time).declination
|
||||
}
|
||||
|
||||
private fun sendPassData(satPass: SatPass, satPos: SatPos, satellite: Satellite) {
|
||||
viewModelScope.launch {
|
||||
var track: List<SatPos> = emptyList()
|
||||
if (!satPass.isDeepSpace) {
|
||||
track = satManager.getTrack(satellite, stationPos, satPass.aosTime, satPass.losTime)
|
||||
}
|
||||
if (settings.getRotatorEnabled()) {
|
||||
val server = settings.getRotatorServer()
|
||||
val port = settings.getRotatorPort().toInt()
|
||||
val azimuth = satPos.azimuth.toDegrees().round(2)
|
||||
val elevation = satPos.elevation.toDegrees().round(2)
|
||||
reporter.reportRotation(server, port, azimuth, elevation)
|
||||
}
|
||||
_passData.postValue(RadarData(satPos, track))
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendPassDataBT(satPos: SatPos) {
|
||||
viewModelScope.launch {
|
||||
if (settings.getBTEnabled()) {
|
||||
val btDevice = settings.getBTDeviceAddr()
|
||||
if (btReporter.isConnected()) {
|
||||
val format = settings.getBTFormat()
|
||||
val azimuth = satPos.azimuth.toDegrees().round(0).toInt()
|
||||
val elevation = satPos.elevation.toDegrees().round(0).toInt()
|
||||
btReporter.reportRotation(format, azimuth, elevation)
|
||||
} else if (!btReporter.isConnecting()) {
|
||||
Log.i("BTReporter", "BTReporter: Attempting to connect...")
|
||||
btReporter.connectBTDevice(btDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processRadios(radios: List<SatRadio>, satellite: Satellite, time: Long) {
|
||||
viewModelScope.launch {
|
||||
delay(125)
|
||||
val list = satManager.processRadios(satellite, stationPos, radios, time)
|
||||
_transmitters.postValue(list)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.radarScreen
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.rtbishop.look4sat.R
|
||||
import com.rtbishop.look4sat.databinding.ItemRadioBinding
|
||||
import com.rtbishop.look4sat.domain.model.SatRadio
|
||||
import java.util.*
|
||||
|
||||
class RadioAdapter : RecyclerView.Adapter<RadioAdapter.TransHolder>() {
|
||||
|
||||
private val diffCallback = object : DiffUtil.ItemCallback<SatRadio>() {
|
||||
override fun areItemsTheSame(oldItem: SatRadio, newItem: SatRadio): Boolean {
|
||||
return oldItem.uuid == newItem.uuid
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: SatRadio, newItem: SatRadio): Boolean {
|
||||
return oldItem.downlink == newItem.downlink
|
||||
}
|
||||
}
|
||||
private val differ = AsyncListDiffer(this, diffCallback)
|
||||
|
||||
fun submitList(items: List<SatRadio>) = differ.submitList(items)
|
||||
|
||||
override fun getItemCount() = differ.currentList.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransHolder {
|
||||
return TransHolder.from(parent)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: TransHolder, position: Int) {
|
||||
holder.bind(differ.currentList[position])
|
||||
}
|
||||
|
||||
class TransHolder private constructor(private val binding: ItemRadioBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
private val divider = 1000000f
|
||||
private val stringYes = itemView.context.getString(R.string.radio_string_yes)
|
||||
private val stringNo = itemView.context.getString(R.string.radio_string_no)
|
||||
private val link = itemView.context.getString(R.string.radio_link_low)
|
||||
private val linkNull = itemView.context.getString(R.string.radio_no_link)
|
||||
private val mode = itemView.context.getString(R.string.radio_mode)
|
||||
private val inverted = itemView.context.getString(R.string.radio_inverted)
|
||||
|
||||
fun bind(radio: SatRadio) {
|
||||
binding.run {
|
||||
radioInfo.text = radio.info
|
||||
radio.downlink.let { downlink ->
|
||||
if (downlink != null) {
|
||||
radioDownlink.text = String.format(Locale.ENGLISH, link, downlink / divider)
|
||||
} else {
|
||||
radioDownlink.text = linkNull
|
||||
}
|
||||
}
|
||||
radio.uplink.let { uplink ->
|
||||
if (uplink != null) {
|
||||
radioUplink.text = String.format(Locale.ENGLISH, link, uplink / divider)
|
||||
} else {
|
||||
radioUplink.text = linkNull
|
||||
}
|
||||
}
|
||||
if (radio.mode != null) {
|
||||
radioMode.text = String.format(mode, radio.mode)
|
||||
} else {
|
||||
radioMode.text = String.format(mode, stringNo)
|
||||
}
|
||||
if (radio.isInverted) {
|
||||
radioInverted.text = String.format(inverted, stringYes)
|
||||
} else {
|
||||
radioInverted.text = String.format(inverted, stringNo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(parent: ViewGroup): TransHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return TransHolder(ItemRadioBinding.inflate(inflater, parent, false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.settingsScreen
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import com.rtbishop.look4sat.R
|
||||
import com.rtbishop.look4sat.databinding.DialogLocatorBinding
|
||||
import com.rtbishop.look4sat.domain.ISettingsManager
|
||||
import com.rtbishop.look4sat.presentation.setNavResult
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class LocatorDialog : AppCompatDialogFragment() {
|
||||
|
||||
@Inject
|
||||
lateinit var preferences: ISettingsManager
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, group: ViewGroup?, state: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.dialog_locator, group, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, state: Bundle?) {
|
||||
super.onViewCreated(view, state)
|
||||
DialogLocatorBinding.bind(view).run {
|
||||
dialog?.window?.setBackgroundDrawableResource(R.color.transparent)
|
||||
dialog?.window?.setLayout(
|
||||
(resources.displayMetrics.widthPixels * 0.94).toInt(),
|
||||
WindowManager.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
locatorEdit.setText(preferences.loadStationLocator())
|
||||
locatorBtnPos.setOnClickListener {
|
||||
setNavResult("locator", locatorEdit.text.toString())
|
||||
dismiss()
|
||||
}
|
||||
locatorBtnNeg.setOnClickListener { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.settingsScreen
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import com.rtbishop.look4sat.R
|
||||
import com.rtbishop.look4sat.databinding.DialogPositionBinding
|
||||
import com.rtbishop.look4sat.domain.ISettingsManager
|
||||
import com.rtbishop.look4sat.presentation.setNavResult
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PositionDialog : AppCompatDialogFragment() {
|
||||
|
||||
@Inject
|
||||
lateinit var preferences: ISettingsManager
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, group: ViewGroup?, state: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.dialog_position, group, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, state: Bundle?) {
|
||||
super.onViewCreated(view, state)
|
||||
DialogPositionBinding.bind(view).run {
|
||||
dialog?.window?.setBackgroundDrawableResource(R.color.transparent)
|
||||
dialog?.window?.setLayout(
|
||||
(resources.displayMetrics.widthPixels * 0.94).toInt(),
|
||||
WindowManager.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
val location = preferences.loadStationPosition()
|
||||
positionLatEdit.setText(location.lat.toString())
|
||||
positionLonEdit.setText(location.lon.toString())
|
||||
positionBtnPos.setOnClickListener {
|
||||
val latitude = try {
|
||||
positionLatEdit.text.toString().toDouble()
|
||||
} catch (exception: Exception) {
|
||||
180.0
|
||||
}
|
||||
val longitude = try {
|
||||
positionLonEdit.text.toString().toDouble()
|
||||
} catch (exception: Exception) {
|
||||
400.0
|
||||
}
|
||||
setNavResult("position", Pair(latitude, longitude))
|
||||
dismiss()
|
||||
}
|
||||
positionBtnNeg.setOnClickListener { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
-307
@@ -1,307 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.settingsScreen
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.rtbishop.look4sat.BuildConfig
|
||||
import com.rtbishop.look4sat.R
|
||||
import com.rtbishop.look4sat.databinding.FragmentSettingsBinding
|
||||
import com.rtbishop.look4sat.domain.model.DataState
|
||||
import com.rtbishop.look4sat.domain.predict.GeoPos
|
||||
import com.rtbishop.look4sat.presentation.clickWithDebounce
|
||||
import com.rtbishop.look4sat.presentation.getNavResult
|
||||
import com.rtbishop.look4sat.utility.isValidIPv4
|
||||
import com.rtbishop.look4sat.utility.isValidPort
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SettingsFragment : Fragment(R.layout.fragment_settings) {
|
||||
|
||||
private val viewModel: SettingsViewModel by viewModels()
|
||||
private val bluetooth = when {
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> Manifest.permission.BLUETOOTH
|
||||
else -> Manifest.permission.BLUETOOTH_CONNECT
|
||||
}
|
||||
private val bluetoothContract = ActivityResultContracts.RequestPermission()
|
||||
private val bluetoothRequest = registerForActivityResult(bluetoothContract) { isGranted ->
|
||||
if (!isGranted) {
|
||||
showToast(getString(R.string.BTremote_perm_error))
|
||||
toggleBTstate(isGranted)
|
||||
}
|
||||
}
|
||||
private val locationFine = Manifest.permission.ACCESS_FINE_LOCATION
|
||||
private val locationCoarse = Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
private val locationContract = ActivityResultContracts.RequestMultiplePermissions()
|
||||
private val locationRequest = registerForActivityResult(locationContract) { permissions ->
|
||||
when {
|
||||
permissions[locationFine] == true -> viewModel.setPositionFromGps()
|
||||
permissions[locationCoarse] == true -> viewModel.setPositionFromNet()
|
||||
else -> showToast(getString(R.string.location_gps_error))
|
||||
}
|
||||
}
|
||||
private val contentContract = ActivityResultContracts.GetContent()
|
||||
private val contentRequest = registerForActivityResult(contentContract) { uri ->
|
||||
uri?.let { viewModel.updateFromFile(uri.toString()) }
|
||||
}
|
||||
private lateinit var binding: FragmentSettingsBinding
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding = FragmentSettingsBinding.bind(view).apply {
|
||||
settingsBtnBack.clickWithDebounce { findNavController().navigateUp() }
|
||||
settingsScroll.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, y, _, newY ->
|
||||
if (y > newY) settingsFab.hide() else settingsFab.show()
|
||||
})
|
||||
settingsAbout.aboutVersion.text =
|
||||
String.format(getString(R.string.app_version), BuildConfig.VERSION_NAME)
|
||||
settingsBtnGithub.clickWithDebounce {
|
||||
gotoUrl("https://github.com/rt-bishop/Look4Sat/")
|
||||
}
|
||||
settingsFab.clickWithDebounce {
|
||||
gotoUrl("https://ko-fi.com/rt_bishop")
|
||||
}
|
||||
settingsBtnFdroid.clickWithDebounce {
|
||||
gotoUrl("https://f-droid.org/en/packages/com.rtbishop.look4sat/")
|
||||
}
|
||||
}
|
||||
setupLocationCard()
|
||||
setupDataCard()
|
||||
setupRemoteCard()
|
||||
setupBTCard()
|
||||
setupOtherCard()
|
||||
setupOutroCard()
|
||||
viewModel.stationPosition.asLiveData().observe(viewLifecycleOwner) { stationPos ->
|
||||
stationPos?.let { handleStationPosition(stationPos) }
|
||||
}
|
||||
viewModel.getUpdateState().asLiveData().observe(viewLifecycleOwner) { updateState ->
|
||||
updateState?.let { handleSatState(updateState) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupLocationCard() {
|
||||
binding.run {
|
||||
setPositionText(viewModel.getStationPosition())
|
||||
settingsLocation.locationBtnGps.clickWithDebounce {
|
||||
locationRequest.launch(arrayOf(locationFine, locationCoarse))
|
||||
}
|
||||
settingsLocation.locationBtnManual.clickWithDebounce {
|
||||
val action = SettingsFragmentDirections.globalToPosition()
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
settingsLocation.locationBtnQth.clickWithDebounce {
|
||||
val action = SettingsFragmentDirections.globalToLocator()
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
getNavResult<Pair<Double, Double>>(R.id.nav_settings, "position") { position ->
|
||||
viewModel.setStationPosition(position.first, position.second)
|
||||
}
|
||||
getNavResult<String>(R.id.nav_settings, "locator") { locator ->
|
||||
viewModel.setPositionFromQth(locator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupDataCard() {
|
||||
binding.run {
|
||||
setUpdateTime(viewModel.getLastUpdateTime())
|
||||
settingsData.dataBtnWeb.clickWithDebounce { viewModel.updateFromWeb() }
|
||||
settingsData.dataBtnFile.clickWithDebounce { contentRequest.launch("*/*") }
|
||||
settingsData.dataBtnClear.clickWithDebounce { viewModel.clearAllData() }
|
||||
viewModel.entriesTotal.observe(viewLifecycleOwner) { number ->
|
||||
val entriesFormat = getString(R.string.data_entries)
|
||||
settingsData.dataEntries.text = String.format(entriesFormat, number)
|
||||
}
|
||||
viewModel.radiosTotal.observe(viewLifecycleOwner) { number ->
|
||||
val radiosFormat = getString(R.string.data_radios)
|
||||
settingsData.dataRadios.text = String.format(radiosFormat, number)
|
||||
}
|
||||
getNavResult<List<String>>(R.id.nav_settings, "sources") {
|
||||
viewModel.updateFromWeb()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRemoteCard() {
|
||||
binding.run {
|
||||
settingsRemote.remoteSwitch.apply {
|
||||
isChecked = viewModel.getRotatorEnabled()
|
||||
settingsRemote.remoteIp.isEnabled = isChecked
|
||||
settingsRemote.remoteIpEdit.setText(viewModel.getRotatorServer())
|
||||
settingsRemote.remotePort.isEnabled = isChecked
|
||||
settingsRemote.remotePortEdit.setText(viewModel.getRotatorPort())
|
||||
setOnCheckedChangeListener { _, isChecked ->
|
||||
viewModel.setRotatorEnabled(isChecked)
|
||||
settingsRemote.remoteIp.isEnabled = isChecked
|
||||
settingsRemote.remotePort.isEnabled = isChecked
|
||||
}
|
||||
}
|
||||
settingsRemote.remoteIpEdit.doOnTextChanged { text, _, _, _ ->
|
||||
if (text.toString().isValidIPv4()) viewModel.setRotatorServer(text.toString())
|
||||
}
|
||||
settingsRemote.remotePortEdit.doOnTextChanged { text, _, _, _ ->
|
||||
if (text.toString().isValidPort()) viewModel.setRotatorPort(text.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupBTCard() {
|
||||
binding.run {
|
||||
settingsBtremote.BTremoteSwitch.apply {
|
||||
isChecked = viewModel.getBTEnabled()
|
||||
settingsBtremote.BTremoteAddress.isEnabled = isChecked
|
||||
settingsBtremote.BTAddressEdit.setText(viewModel.getBTDeviceAddr())
|
||||
settingsBtremote.BTremoteFormat.isEnabled = isChecked
|
||||
settingsBtremote.BTFormatEdit.setText(viewModel.getBTFormat())
|
||||
setOnCheckedChangeListener { _, isChecked ->
|
||||
toggleBTstate(isChecked)
|
||||
bluetoothRequest.launch(bluetooth)
|
||||
}
|
||||
}
|
||||
settingsBtremote.BTAddressEdit.doOnTextChanged { text, _, _, _ ->
|
||||
viewModel.setBTDeviceAddr(text.toString())
|
||||
}
|
||||
settingsBtremote.BTFormatEdit.doOnTextChanged { text, _, _, _ ->
|
||||
viewModel.setBTFormat(text.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleBTstate(value: Boolean) {
|
||||
binding.run {
|
||||
viewModel.setBTEnabled(value)
|
||||
settingsBtremote.BTremoteSwitch.isChecked = value
|
||||
settingsBtremote.BTremoteAddress.isEnabled = value
|
||||
settingsBtremote.BTremoteFormat.isEnabled = value
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupOtherCard() {
|
||||
binding.run {
|
||||
settingsOther.otherSwitchUtc.apply {
|
||||
isChecked = viewModel.getUseUTC()
|
||||
setOnCheckedChangeListener { _, isChecked -> viewModel.setUseUTC(isChecked) }
|
||||
}
|
||||
settingsOther.otherSwitchUpdate.apply {
|
||||
isChecked = viewModel.getAutoUpdateEnabled()
|
||||
setOnCheckedChangeListener { _, isChecked -> viewModel.setAutoUpdateEnabled(isChecked) }
|
||||
}
|
||||
settingsOther.otherSwitchSweep.apply {
|
||||
isChecked = viewModel.getShowSweep()
|
||||
setOnCheckedChangeListener { _, isChecked -> viewModel.setShowSweep(isChecked) }
|
||||
}
|
||||
settingsOther.otherSwitchSensors.apply {
|
||||
isChecked = viewModel.getUseCompass()
|
||||
setOnCheckedChangeListener { _, isChecked -> viewModel.setUseCompass(isChecked) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupOutroCard() {
|
||||
binding.run {
|
||||
settingsOutro.outroThanks.movementMethod = LinkMovementMethod.getInstance()
|
||||
settingsOutro.outroLicense.movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleStationPosition(pos: DataState<GeoPos>) {
|
||||
when (pos) {
|
||||
is DataState.Success -> {
|
||||
setPositionText(pos.data)
|
||||
binding.settingsLocation.locationProgress.isIndeterminate = false
|
||||
viewModel.setPositionHandled()
|
||||
showToast(getString(R.string.location_success))
|
||||
}
|
||||
is DataState.Error -> {
|
||||
binding.settingsLocation.locationProgress.isIndeterminate = false
|
||||
viewModel.setPositionHandled()
|
||||
showToast(pos.message.toString())
|
||||
}
|
||||
DataState.Loading -> {
|
||||
binding.settingsLocation.locationProgress.isIndeterminate = true
|
||||
}
|
||||
DataState.Handled -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPositionText(geoPos: GeoPos) {
|
||||
binding.run {
|
||||
val latFormat = getString(R.string.location_lat)
|
||||
val lonFormat = getString(R.string.location_lon)
|
||||
settingsLocation.locationLat.text = String.format(latFormat, geoPos.lat)
|
||||
settingsLocation.locationLon.text = String.format(lonFormat, geoPos.lon)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSatState(state: DataState<Long>) {
|
||||
when (state) {
|
||||
is DataState.Success -> {
|
||||
binding.settingsData.dataProgress.isIndeterminate = false
|
||||
setUpdateTime(state.data)
|
||||
viewModel.setUpdateHandled()
|
||||
if (state.data == 0L) {
|
||||
showToast(getString(R.string.data_clear_success))
|
||||
} else {
|
||||
showToast(getString(R.string.data_success))
|
||||
}
|
||||
}
|
||||
is DataState.Error -> {
|
||||
binding.settingsData.dataProgress.isIndeterminate = false
|
||||
viewModel.setUpdateHandled()
|
||||
showToast(getString(R.string.data_error))
|
||||
}
|
||||
is DataState.Loading -> {
|
||||
binding.settingsData.dataProgress.isIndeterminate = true
|
||||
}
|
||||
is DataState.Handled -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUpdateTime(updateTime: Long) {
|
||||
val updatePattern = getString(R.string.data_update)
|
||||
val updateDate = if (updateTime == 0L) {
|
||||
getString(R.string.pass_placeholder)
|
||||
} else {
|
||||
SimpleDateFormat("d MMM yyyy - HH:mm:ss", Locale.getDefault()).format(Date(updateTime))
|
||||
}
|
||||
binding.settingsData.dataUpdate.text = String.format(updatePattern, updateDate)
|
||||
}
|
||||
|
||||
private fun showToast(message: String) {
|
||||
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun gotoUrl(url: String) {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||
}
|
||||
}
|
||||
-110
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.presentation.settingsScreen
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import com.rtbishop.look4sat.domain.IDataRepository
|
||||
import com.rtbishop.look4sat.domain.ILocationManager
|
||||
import com.rtbishop.look4sat.domain.ISettingsManager
|
||||
import com.rtbishop.look4sat.domain.model.DataState
|
||||
import com.rtbishop.look4sat.domain.predict.GeoPos
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsViewModel @Inject constructor(
|
||||
private val locationManager: ILocationManager,
|
||||
private val repository: IDataRepository,
|
||||
private val settings: ISettingsManager
|
||||
) : ViewModel() {
|
||||
|
||||
val entriesTotal = repository.getEntriesTotal().asLiveData()
|
||||
val radiosTotal = repository.getRadiosTotal().asLiveData()
|
||||
|
||||
fun updateFromFile(uri: String) = repository.updateFromFile(uri)
|
||||
|
||||
fun updateFromWeb() = repository.updateFromWeb()
|
||||
|
||||
fun clearAllData() = repository.clearAllData()
|
||||
|
||||
fun getUseUTC(): Boolean = settings.getUseUTC()
|
||||
|
||||
fun setUseUTC(value: Boolean) = settings.setUseUTC(value)
|
||||
|
||||
fun getLastUpdateTime(): Long = settings.getLastUpdateTime()
|
||||
|
||||
fun getAutoUpdateEnabled(): Boolean = settings.getAutoUpdateEnabled()
|
||||
|
||||
fun setAutoUpdateEnabled(value: Boolean) = settings.setAutoUpdateEnabled(value)
|
||||
|
||||
fun getUseCompass(): Boolean = settings.getUseCompass()
|
||||
|
||||
fun setUseCompass(value: Boolean) = settings.setUseCompass(value)
|
||||
|
||||
fun getShowSweep(): Boolean = settings.getShowSweep()
|
||||
|
||||
fun setShowSweep(value: Boolean) = settings.setShowSweep(value)
|
||||
|
||||
fun getRotatorEnabled(): Boolean = settings.getRotatorEnabled()
|
||||
|
||||
fun setRotatorEnabled(value: Boolean) = settings.setRotatorEnabled(value)
|
||||
|
||||
fun getRotatorServer(): String = settings.getRotatorServer()
|
||||
|
||||
fun setRotatorServer(value: String) = settings.setRotatorServer(value)
|
||||
|
||||
fun getRotatorPort(): String = settings.getRotatorPort()
|
||||
|
||||
fun setRotatorPort(value: String) = settings.setRotatorPort(value)
|
||||
|
||||
fun getBTEnabled(): Boolean = settings.getBTEnabled()
|
||||
|
||||
fun setBTEnabled(value: Boolean) = settings.setBTEnabled(value)
|
||||
|
||||
fun getBTFormat(): String = settings.getBTFormat()
|
||||
|
||||
fun setBTFormat(value: String) = settings.setBTFormat(value)
|
||||
|
||||
// fun getBTDeviceName(): String = settings.getBTDeviceName()
|
||||
|
||||
// fun setBTDeviceName(value: String) = settings.setBTDeviceName(value)
|
||||
|
||||
fun getBTDeviceAddr(): String = settings.getBTDeviceAddr()
|
||||
|
||||
fun setBTDeviceAddr(value: String) = settings.setBTDeviceAddr(value)
|
||||
|
||||
fun getUpdateState() = repository.updateState
|
||||
|
||||
fun setUpdateHandled() = repository.setUpdateStateHandled()
|
||||
|
||||
val stationPosition: SharedFlow<DataState<GeoPos>> = locationManager.stationPosition
|
||||
|
||||
fun getStationPosition(): GeoPos = locationManager.getStationPosition()
|
||||
|
||||
fun setStationPosition(lat: Double, lon: Double) = locationManager.setStationPosition(lat, lon)
|
||||
|
||||
fun setPositionFromGps() = locationManager.setPositionFromGps()
|
||||
|
||||
fun setPositionFromNet() = locationManager.setPositionFromNet()
|
||||
|
||||
fun setPositionFromQth(qthString: String) = locationManager.setPositionFromQth(qthString)
|
||||
|
||||
fun setPositionHandled() = locationManager.setPositionHandled()
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
|
||||
</vector>
|
||||
@@ -1,11 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
|
||||
</vector>
|
||||
@@ -1,11 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18,7l-1.41,-1.41 -6.34,6.34 1.41,1.41L18,7zM22.24,5.59L11.66,16.17 7.48,12l-1.41,1.41L11.66,19l12,-12 -1.42,-1.41zM0.41,13.41L6,19l1.41,-1.41L1.83,12 0.41,13.41z" />
|
||||
</vector>
|
||||
@@ -1,4 +0,0 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/background" />
|
||||
</shape>
|
||||
@@ -1,21 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="271.2439"
|
||||
android:viewportHeight="271.2439">
|
||||
<group
|
||||
android:translateX="1.3562195"
|
||||
android:translateY="1.3562195">
|
||||
<group
|
||||
android:translateX="70.26573"
|
||||
android:translateY="70.26573">
|
||||
<path
|
||||
android:fillColor="@color/accent"
|
||||
android:pathData="m96.642,31.504c7.911,7.997 17.143,13.584 25.365,15.581l1.754,-3.997C123.78,43.048 123.786,43.005 123.802,42.965 125.834,38.995 127,34.495 127,29.715 127,13.881 114.379,1 98.867,1c-5.125,0 -9.92,1.429 -14.064,3.884l-3.398,1.598c0.08,0.306 0.133,0.599 0.221,0.91 2.32,8.104 7.652,16.667 15.016,24.112z" />
|
||||
<path
|
||||
android:fillColor="@color/accent"
|
||||
android:pathData="M93.876,34.3C86.053,26.391 80.369,17.225 77.87,8.49 77.84,8.385 77.818,8.285 77.788,8.181L2.629,43.513c-1.404,0.66 -2.019,2.358 -1.372,3.791 0.472,1.046 1.485,1.663 2.545,1.663 0.392,0 0.79,-0.084 1.169,-0.263L74.045,16.235c-1.873,3.578 -2.997,7.611 -3.228,11.885L35.566,51.732c-1.294,0.866 -1.653,2.638 -0.805,3.957 0.538,0.836 1.431,1.29 2.344,1.29 0.526,0 1.06,-0.151 1.533,-0.469L71.189,34.707c1.4,8.11 6.135,15.044 12.715,19.285l-54.945,68.337c-0.981,1.22 -0.808,3.021 0.388,4.023 0.521,0.437 1.149,0.649 1.775,0.649 0.809,0 1.611,-0.357 2.166,-1.045L89.056,56.595c3.06,1.168 6.357,1.838 9.811,1.838 4.932,0 9.566,-1.312 13.601,-3.597L84.602,118.323c-0.633,1.44 0,3.131 1.41,3.777 0.372,0.17 0.76,0.25 1.144,0.25 1.07,0 2.092,-0.629 2.556,-1.691L120.402,50.739C111.642,48.416 102.042,42.555 93.876,34.3Z" />
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -1,11 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M2,21h19v-3H2v3zM20,8H3c-0.55,0 -1,0.45 -1,1v6c0,0.55 0.45,1 1,1h17c0.55,0 1,-0.45 1,-1V9c0,-0.55 -0.45,-1 -1,-1zM2,3v3h19V3H2z" />
|
||||
</vector>
|
||||
@@ -1,11 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.95,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z" />
|
||||
</vector>
|
||||
@@ -1,23 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="128"
|
||||
android:viewportHeight="128">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m105.138,114.121a2.864,2.864 90,0 0,1.701 -0.85l19.334,-19.331a2.864,2.864 90,0 0,-0 -4.027l-26.584,-26.625a2.864,2.864 90,0 0,-1.835 -0.805l-7.922,-0.626 5.773,-5.772a2.864,2.864 90,0 0,-0 -4.027l-19.602,-19.644a2.864,2.864 90,0 0,-4.073 -0l-5.773,5.772 -0.627,-7.92a2.864,2.864 90,0 0,-0.805 -1.835l-26.629,-26.58a2.864,2.864 90,0 0,-4.073 -0l-19.289,19.286a2.864,2.864 90,0 0,-0 4.072l26.584,26.625a2.864,2.864 90,0 0,1.835 0.805l7.922,0.626 -5.773,5.772a2.864,2.864 90,0 0,-0 4.072l19.603,19.599a2.864,2.864 90,0 0,4.073 -0l5.773,-5.772 0.626,7.92a2.864,2.864 90,0 0,0.806 1.835l26.629,26.58a2.864,2.864 90,0 0,2.059 0.85,2.864 2.864,90 0,0 0.269,-0zM104.825,107.23 L80.97,83.38 80.165,72.685 85.58,67.271 96.276,68.121 120.13,91.926zM55.326,47.85 L44.629,47 20.775,23.194 36.081,7.891 59.891,31.741 60.741,42.436z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m62.653,115.806a2.864,2.864 90,1 0,-0 -5.727c-11.457,-0 -22.876,-4.351 -31.634,-13.107 -8.741,-8.74 -13.095,-20.174 -13.095,-31.614a2.864,2.864 90,1 0,-5.728 -0c-0,12.897 4.935,25.829 14.773,35.664 9.85,9.848 22.772,14.785 35.684,14.785z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m62.653,127a2.864,2.864 90,1 0,-0 -5.727c-14.325,-0 -28.612,-5.441 -39.547,-16.375 -10.935,-10.933 -16.378,-25.233 -16.378,-39.541a2.864,2.864 90,1 0,-5.728 -0c-0,15.763 6.025,31.562 18.055,43.59 12.029,12.028 27.818,18.052 43.597,18.052z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m62.653,104.698a2.864,2.864 90,1 0,-0 -5.727c-8.62,-0 -17.201,-3.263 -23.778,-9.839 -6.577,-6.576 -9.855,-15.174 -9.855,-23.774a2.864,2.864 90,1 0,-5.728 -0c-0,10.061 3.861,20.153 11.533,27.824 7.672,7.671 17.756,11.517 27.829,11.517z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m62.653,93.475a2.864,2.864 90,1 0,-0 -5.727c-5.737,-0 -11.453,-2.174 -15.836,-6.557 -4.384,-4.383 -6.558,-10.098 -6.558,-15.834a2.864,2.864 90,1 0,-5.728 -0c-0,7.192 2.757,14.406 8.236,19.883 5.479,5.478 12.693,8.234 19.887,8.234z" />
|
||||
</vector>
|
||||
@@ -1,11 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M8,6v14l11,-7z" />
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/accent"
|
||||
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z" />
|
||||
</vector>
|
||||
@@ -1,11 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M16,6v14l-11,-7z" />
|
||||
</vector>
|
||||
@@ -1,11 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,8l-4,4h3c0,3.31 -2.69,6 -6,6 -1.01,0 -1.97,-0.25 -2.8,-0.7l-1.46,1.46C8.97,19.54 10.43,20 12,20c4.42,0 8,-3.58 8,-8h3l-4,-4zM6,12c0,-3.31 2.69,-6 6,-6 1.01,0 1.97,0.25 2.8,0.7l1.46,-1.46C15.03,4.46 13.57,4 12,4c-4.42,0 -8,3.58 -8,8H1l4,4 4,-4H6z" />
|
||||
</vector>
|
||||
@@ -1,14 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="128"
|
||||
android:viewportHeight="128">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m96.642,31.504c7.911,7.997 17.143,13.584 25.365,15.581l1.754,-3.997C123.78,43.048 123.786,43.005 123.802,42.965 125.834,38.995 127,34.495 127,29.715 127,13.881 114.379,1 98.867,1c-5.125,0 -9.92,1.429 -14.064,3.884l-3.398,1.598c0.08,0.306 0.133,0.599 0.221,0.91 2.32,8.104 7.652,16.667 15.016,24.112z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M93.876,34.3C86.053,26.391 80.369,17.225 77.87,8.49 77.84,8.385 77.818,8.285 77.788,8.181L2.629,43.513c-1.404,0.66 -2.019,2.358 -1.372,3.791 0.472,1.046 1.485,1.663 2.545,1.663 0.392,0 0.79,-0.084 1.169,-0.263L74.045,16.235c-1.873,3.578 -2.997,7.611 -3.228,11.885L35.566,51.732c-1.294,0.866 -1.653,2.638 -0.805,3.957 0.538,0.836 1.431,1.29 2.344,1.29 0.526,0 1.06,-0.151 1.533,-0.469L71.189,34.707c1.4,8.11 6.135,15.044 12.715,19.285l-54.945,68.337c-0.981,1.22 -0.808,3.021 0.388,4.023 0.521,0.437 1.149,0.649 1.775,0.649 0.809,0 1.611,-0.357 2.166,-1.045L89.056,56.595c3.06,1.168 6.357,1.838 9.811,1.838 4.932,0 9.566,-1.312 13.601,-3.597L84.602,118.323c-0.633,1.44 0,3.131 1.41,3.777 0.372,0.17 0.76,0.25 1.144,0.25 1.07,0 2.092,-0.629 2.556,-1.691L120.402,50.739C111.642,48.416 102.042,42.555 93.876,34.3Z" />
|
||||
</vector>
|
||||
@@ -1,13 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="192dp"
|
||||
android:height="200dp"
|
||||
android:viewportWidth="210"
|
||||
android:viewportHeight="220">
|
||||
<group
|
||||
android:translateX="52"
|
||||
android:translateY="44">
|
||||
<path
|
||||
android:fillColor="@color/accent"
|
||||
android:pathData="m104.005,111.936q0.504,0 0.958,-0.063 0.467,-0.076 0.87,-0.176l0,1.674q-0.202,0.101 -0.555,0.214 -0.34,0.113 -0.807,0.201 -0.454,0.101 -1.034,0.164 -0.58,0.063 -1.261,0.063 -0.87,0 -1.576,-0.214 -0.706,-0.201 -1.198,-0.68 -0.492,-0.491 -0.757,-1.259 -0.265,-0.78 -0.265,-1.913l0,-8.244l-1.942,0l0,-1.322q0.832,0 1.488,-0.327 0.656,-0.327 1.047,-0.755 0.794,-0.831 1.185,-2.555l2.144,0l0,3.197l3.304,0l0,1.762l-3.304,0l0,7.98q0,1.171 0.391,1.712 0.404,0.541 1.311,0.541zM85.595,109.922q0,1.108 0.353,1.661 0.353,0.554 1.11,0.554 0.555,0 0.996,-0.227 0.454,-0.239 0.769,-0.667 0.328,-0.428 0.492,-1.032 0.177,-0.617 0.177,-1.372l0,-1.938l-1.16,0.076q-0.769,0.038 -1.299,0.252 -0.53,0.201 -0.845,0.566 -0.315,0.365 -0.454,0.894 -0.139,0.529 -0.139,1.233zM87.89,101.188q-0.492,0 -0.832,0.214 -0.328,0.201 -0.53,0.579 -0.202,0.365 -0.29,0.881 -0.076,0.503 -0.076,1.108 -1.74,0 -2.623,-0.39 -0.87,-0.39 -0.87,-1.334 0,-0.705 0.416,-1.208 0.429,-0.503 1.147,-0.818 0.731,-0.315 1.69,-0.453 0.971,-0.151 2.068,-0.151 1.362,0 2.371,0.239 1.021,0.227 1.69,0.743 0.681,0.516 1.021,1.359 0.34,0.831 0.34,2.039l0,6.57q0,0.529 0.076,0.881 0.088,0.352 0.265,0.566 0.177,0.201 0.479,0.289 0.303,0.088 0.731,0.088l0.101,0l0,1.372l-4.804,0l-0.542,-1.775l-0.227,0q-0.429,0.516 -0.807,0.906 -0.378,0.378 -0.807,0.629 -0.429,0.252 -0.971,0.365 -0.53,0.126 -1.261,0.126 -0.857,0 -1.601,-0.252 -0.731,-0.252 -1.286,-0.768 -0.542,-0.529 -0.857,-1.309 -0.315,-0.793 -0.315,-1.863 0,-2.102 1.438,-3.109 1.45,-1.007 4.35,-1.095l2.118,-0.076l0,-1.422q0,-0.642 -0.05,-1.183 -0.05,-0.541 -0.214,-0.931 -0.164,-0.39 -0.492,-0.604 -0.315,-0.214 -0.845,-0.214zM72.115,114.013q-1.74,0 -2.9,-0.327 -1.16,-0.327 -1.866,-0.843 -0.694,-0.516 -0.996,-1.158 -0.29,-0.642 -0.29,-1.271 0,-0.667 0.24,-1.12 0.252,-0.453 0.656,-0.743 0.416,-0.289 0.933,-0.415 0.53,-0.126 1.097,-0.126 0,1.108 0.265,1.951 0.265,0.831 0.731,1.397 0.467,0.554 1.11,0.843 0.656,0.277 1.425,0.277 0.757,0 1.349,-0.214 0.593,-0.214 1.009,-0.592 0.416,-0.378 0.63,-0.881 0.214,-0.516 0.214,-1.108 0,-0.654 -0.265,-1.158 -0.265,-0.516 -0.807,-0.944 -0.53,-0.441 -1.349,-0.831 -0.807,-0.403 -1.891,-0.856 -1.349,-0.554 -2.282,-1.133 -0.921,-0.592 -1.488,-1.271 -0.567,-0.68 -0.82,-1.473 -0.24,-0.793 -0.24,-1.737 0,-1.183 0.479,-2.14 0.479,-0.957 1.324,-1.636 0.845,-0.68 2.005,-1.045 1.16,-0.365 2.522,-0.365 1.425,0 2.459,0.239 1.034,0.227 1.702,0.629 0.681,0.403 1.009,0.957 0.328,0.541 0.328,1.171 0,0.453 -0.202,0.843 -0.189,0.39 -0.593,0.68 -0.391,0.289 -0.996,0.453 -0.593,0.164 -1.387,0.164 0,-0.541 -0.151,-1.158 -0.139,-0.629 -0.467,-1.158 -0.328,-0.541 -0.857,-0.894 -0.517,-0.352 -1.261,-0.352 -0.517,0 -0.984,0.151 -0.467,0.151 -0.82,0.453 -0.34,0.302 -0.542,0.755 -0.202,0.441 -0.202,1.019 0,0.541 0.177,1.019 0.177,0.478 0.643,0.944 0.479,0.453 1.299,0.919 0.832,0.453 2.144,0.982 1.324,0.541 2.257,1.108 0.933,0.566 1.526,1.221 0.605,0.642 0.87,1.422 0.277,0.768 0.277,1.712 0,1.246 -0.492,2.291 -0.492,1.032 -1.412,1.775 -0.921,0.743 -2.219,1.158 -1.286,0.415 -2.9,0.415zM62.115,108.966l0,1.573q0,0.604 0.164,0.969 0.177,0.365 0.454,0.566 0.29,0.189 0.681,0.252 0.391,0.063 0.82,0.063l0.328,0l0,1.372l-9.054,0l0,-1.372l0.719,0q0.441,0 0.82,-0.063 0.391,-0.063 0.668,-0.252 0.29,-0.201 0.454,-0.566 0.177,-0.365 0.177,-0.969l0,-1.573l-7.15,0l0,-1.573l7.326,-12.032l3.594,0l0,11.906l2.686,0l0,1.699zM58.345,102.761q0,-1.133 0.038,-2.442 0.038,-1.309 0.139,-2.517 -0.126,0.302 -0.404,0.831 -0.277,0.529 -0.593,1.095 -0.315,0.554 -0.605,1.057 -0.29,0.491 -0.441,0.73l-3.48,5.752l5.347,0zM50.855,113.761l-0.366,0q-1.412,0 -2.371,-0.063 -0.958,-0.063 -1.652,-0.289 -0.694,-0.227 -1.198,-0.68 -0.504,-0.466 -1.009,-1.259l-2.295,-3.612 -1.034,0.768l0,2.089q0,0.554 0.177,0.881 0.189,0.327 0.454,0.503 0.277,0.176 0.605,0.239 0.328,0.05 0.605,0.05l0.303,0l0,1.372L34.866,113.761l0,-1.372l0.303,0q0.29,0 0.605,-0.05 0.328,-0.063 0.593,-0.239 0.277,-0.176 0.454,-0.503 0.189,-0.327 0.189,-0.881l0,-13.606q0,-0.516 -0.189,-0.818 -0.177,-0.315 -0.454,-0.478 -0.277,-0.176 -0.605,-0.227 -0.315,-0.05 -0.593,-0.05l-0.303,0l0,-1.359l6.065,0l0,9.943q0,0.352 -0.025,0.818 -0.013,0.453 -0.038,0.868 -0.038,0.491 -0.063,0.994l3.253,-3.637q0.429,-0.478 0.555,-0.755 0.139,-0.289 0.139,-0.478 0,-0.327 -0.353,-0.503 -0.34,-0.189 -0.984,-0.189l0,-1.296l6.507,0l0,1.296q-0.895,0 -1.765,0.529 -0.857,0.516 -1.879,1.661l-1.627,1.85 3.43,5.11q0.681,1.019 1.374,1.51 0.694,0.491 1.311,0.491l0.088,0zM33.517,106.31q0,1.007 -0.29,1.901 -0.29,0.894 -0.82,1.573 -0.517,0.667 -1.261,1.057 -0.731,0.39 -1.627,0.39 -1.198,0 -2.333,-0.73 -1.122,-0.743 -2.055,-2.417 -0.794,1.523 -1.828,2.303 -1.034,0.768 -2.181,0.768 -0.883,0 -1.627,-0.34 -0.744,-0.352 -1.286,-0.982 -0.542,-0.642 -0.845,-1.548 -0.29,-0.919 -0.29,-2.052 0,-1.032 0.29,-1.926 0.29,-0.906 0.82,-1.561 0.53,-0.667 1.274,-1.045 0.744,-0.39 1.664,-0.39 0.593,0 1.173,0.176 0.593,0.164 1.147,0.541 0.567,0.365 1.072,0.944 0.517,0.579 0.946,1.41 0.794,-1.498 1.791,-2.24 0.996,-0.755 2.295,-0.755Line truncated
|
||||
</group>
|
||||
</vector>
|
||||
@@ -1,11 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M14,21c1.93,0 3.62,-1.17 4,-3l-1.75,-0.88C16,18.21 15.33,19 14,19l-4.9,0c0.83,-1 1.5,-2.34 1.5,-4c0,-0.35 -0.03,-0.69 -0.08,-1L14,14v-2l-4.18,0C9,10.42 8,9.6 8,8c0,-1.93 1.57,-3.5 3.5,-3.5c1.5,0 2.79,0.95 3.28,2.28L16.63,6c-0.8,-2.05 -2.79,-3.5 -5.13,-3.5C8.46,2.5 6,4.96 6,8c0,1.78 0.79,2.9 1.49,4L6,12v2l2.47,0c0.08,0.31 0.13,0.64 0.13,1c0,2.7 -2.6,4 -2.6,4v2H14z" />
|
||||
</vector>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<size
|
||||
android:width="1dp"
|
||||
android:height="1dp" />
|
||||
<solid android:color="@color/background" />
|
||||
</shape>
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/nav_host"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/nav_graph" />
|
||||
@@ -1,73 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/SurfaceCard">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/about_img"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:src="@drawable/ic_satellite"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/about_version"
|
||||
app:layout_constraintDimensionRatio="h,1:1"
|
||||
app:layout_constraintEnd_toStartOf="@+id/about_title"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/about_title"
|
||||
app:tint="@color/accent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/about_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:includeFontPadding="false"
|
||||
android:text="@string/app_name"
|
||||
android:textColor="@color/accent"
|
||||
android:textSize="@dimen/text_size_app_title"
|
||||
app:layout_constraintBottom_toTopOf="@+id/about_version"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/about_img"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/about_version"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/app_version"
|
||||
android:textColor="@color/textMain"
|
||||
android:textSize="@dimen/text_size_large"
|
||||
app:layout_constraintBottom_toTopOf="@+id/about_subtitle"
|
||||
app:layout_constraintEnd_toEndOf="@+id/about_title"
|
||||
app:layout_constraintStart_toStartOf="@+id/about_title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/about_title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/about_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:text="@string/app_subtitle"
|
||||
android:textColor="@color/textMain"
|
||||
android:textSize="@dimen/text_size_large"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/about_version" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
@@ -1,74 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/SurfaceCard">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/BTremote_title"
|
||||
style="@style/SettingsTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:text="@string/BTremote_title"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/BTremote_switch"
|
||||
style="@style/SettingsText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="42dp"
|
||||
android:text="@string/BTremote_switch"
|
||||
app:layout_constraintEnd_toEndOf="@+id/BTremote_title"
|
||||
app:layout_constraintStart_toStartOf="@+id/BTremote_title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/BTremote_title"
|
||||
app:trackTint="@color/textDisabled" />
|
||||
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/BTremote_address"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="@+id/BTremote_switch"
|
||||
app:layout_constraintStart_toStartOf="@+id/BTremote_switch"
|
||||
app:layout_constraintTop_toBottomOf="@id/BTremote_switch">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/BT_address_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:hint="@string/BTremote_device_hint"
|
||||
android:textColorHint="@color/textMain" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/BTremote_format"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@+id/BTremote_address"
|
||||
app:layout_constraintStart_toStartOf="@+id/BTremote_address"
|
||||
app:layout_constraintTop_toBottomOf="@id/BTremote_address">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/BT_format_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:hint="@string/BTremote_output_hint"
|
||||
android:textColorHint="@color/textMain" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
@@ -1,107 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/SurfaceCard">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/data_title"
|
||||
style="@style/SettingsTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/data_title"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/data_progress"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/data_title"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/data_title"
|
||||
app:layout_constraintTop_toTopOf="@+id/data_title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/data_update"
|
||||
style="@style/SettingsText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:text="@string/data_update"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toStartOf="@+id/data_title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/data_title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/data_entries"
|
||||
style="@style/SettingsText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:text="@string/data_entries"
|
||||
app:layout_constraintEnd_toStartOf="@+id/data_radios"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toStartOf="@+id/data_update"
|
||||
app:layout_constraintTop_toBottomOf="@+id/data_update" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/data_radios"
|
||||
style="@style/SettingsText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/data_radios"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/data_entries"
|
||||
app:layout_constraintEnd_toEndOf="@+id/data_progress"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/data_entries" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/data_btn_web"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:text="@string/btn_web"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/data_btn_file"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/data_entries" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/data_btn_file"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:text="@string/btn_import"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/data_btn_web"
|
||||
app:layout_constraintEnd_toStartOf="@+id/data_btn_clear"
|
||||
app:layout_constraintStart_toEndOf="@+id/data_btn_web" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/data_btn_clear"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:text="@string/btn_clear"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/data_btn_web"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/data_btn_file" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
@@ -1,99 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/SurfaceCard">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/location_title"
|
||||
style="@style/SettingsTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/location_title"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/location_progress"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/location_title"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/location_title"
|
||||
app:layout_constraintTop_toTopOf="@+id/location_title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/location_lat"
|
||||
style="@style/SettingsText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:text="@string/location_lat"
|
||||
app:layout_constraintEnd_toStartOf="@+id/location_lon"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toStartOf="@+id/location_title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/location_title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/location_lon"
|
||||
style="@style/SettingsText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/location_lon"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/location_lat"
|
||||
app:layout_constraintEnd_toEndOf="@+id/location_progress"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/location_lat" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/location_btn_gps"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:text="@string/btn_gps"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/location_btn_manual"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/location_lat" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/location_btn_manual"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:text="@string/btn_manual"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/location_btn_gps"
|
||||
app:layout_constraintEnd_toStartOf="@+id/location_btn_qth"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/location_btn_gps" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/location_btn_qth"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:text="@string/btn_qth"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/location_btn_gps"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/location_btn_manual" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
@@ -1,81 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/SurfaceCard">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/other_title"
|
||||
style="@style/SettingsTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:text="@string/other_title"
|
||||
app:layout_constraintBottom_toTopOf="@+id/other_switch_utc"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/other_switch_utc"
|
||||
style="@style/SettingsText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:minHeight="42dp"
|
||||
android:text="@string/other_switch_utc"
|
||||
app:layout_constraintBottom_toTopOf="@+id/other_switch_update"
|
||||
app:layout_constraintEnd_toEndOf="@+id/other_title"
|
||||
app:layout_constraintStart_toStartOf="@+id/other_title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/other_title"
|
||||
app:trackTint="@color/textDisabled" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/other_switch_update"
|
||||
style="@style/SettingsText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="42dp"
|
||||
android:text="@string/other_switch_update"
|
||||
app:layout_constraintBottom_toTopOf="@+id/other_switch_sweep"
|
||||
app:layout_constraintEnd_toEndOf="@+id/other_switch_utc"
|
||||
app:layout_constraintStart_toStartOf="@+id/other_switch_utc"
|
||||
app:layout_constraintTop_toBottomOf="@+id/other_switch_utc"
|
||||
app:trackTint="@color/textDisabled" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/other_switch_sweep"
|
||||
style="@style/SettingsText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="42dp"
|
||||
android:text="@string/other_switch_sweep"
|
||||
app:layout_constraintBottom_toTopOf="@+id/other_switch_sensors"
|
||||
app:layout_constraintEnd_toEndOf="@+id/other_switch_update"
|
||||
app:layout_constraintStart_toStartOf="@+id/other_switch_update"
|
||||
app:layout_constraintTop_toBottomOf="@+id/other_switch_update"
|
||||
app:trackTint="@color/textDisabled" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/other_switch_sensors"
|
||||
style="@style/SettingsText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:minHeight="42dp"
|
||||
android:text="@string/other_switch_sensors"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@+id/other_switch_sweep"
|
||||
app:layout_constraintStart_toStartOf="@+id/other_switch_sweep"
|
||||
app:layout_constraintTop_toBottomOf="@+id/other_switch_sweep"
|
||||
app:trackTint="@color/textDisabled" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
@@ -1,53 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/SurfaceCard">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/outro_title"
|
||||
style="@style/SettingsTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:text="@string/outro_title"
|
||||
app:layout_constraintBottom_toTopOf="@+id/outro_thanks"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/outro_thanks"
|
||||
style="@style/SettingsText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/outro_thanks"
|
||||
app:layout_constraintBottom_toTopOf="@+id/outro_license"
|
||||
app:layout_constraintEnd_toEndOf="@+id/outro_title"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/outro_title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/outro_title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/outro_license"
|
||||
style="@style/SettingsText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:text="@string/outro_license"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@+id/outro_title"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/outro_title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/outro_thanks" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
@@ -1,79 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/SurfaceCard">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/remote_title"
|
||||
style="@style/SettingsTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:text="@string/remote_title"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/remote_switch"
|
||||
style="@style/SettingsText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="42dp"
|
||||
android:text="@string/remote_switch"
|
||||
app:layout_constraintEnd_toEndOf="@+id/remote_title"
|
||||
app:layout_constraintStart_toStartOf="@+id/remote_title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/remote_title"
|
||||
app:trackTint="@color/textDisabled" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/remote_ip"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/remote_port"
|
||||
app:layout_constraintStart_toStartOf="@+id/remote_switch"
|
||||
app:layout_constraintTop_toBottomOf="@+id/remote_switch"
|
||||
app:layout_constraintWidth_percent="0.64">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/remote_ip_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:digits="0123456789."
|
||||
android:hint="@string/remote_ip_hint"
|
||||
android:inputType="number"
|
||||
android:textColorHint="@color/textMain" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/remote_port"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/remote_ip"
|
||||
app:layout_constraintEnd_toEndOf="@+id/remote_switch"
|
||||
app:layout_constraintStart_toEndOf="@+id/remote_ip"
|
||||
app:layout_constraintTop_toTopOf="@+id/remote_ip">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/remote_port_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/remote_port_hint"
|
||||
android:inputType="number"
|
||||
android:textColorHint="@color/textMain" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
@@ -1,131 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/SurfaceCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/filter_title"
|
||||
style="@style/DialogTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:minHeight="48dp"
|
||||
android:text="@string/filter_title"
|
||||
app:layout_constraintBottom_toTopOf="@+id/filter_hours_text"
|
||||
app:layout_constraintEnd_toEndOf="@+id/filter_btn_pos"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/filter_btn_neg"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/filter_hours_text"
|
||||
style="@style/DialogText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/filter_hours_text"
|
||||
app:layout_constraintBottom_toTopOf="@+id/filter_hours_layout"
|
||||
app:layout_constraintEnd_toEndOf="@+id/filter_title"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/filter_title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/filter_title" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/filter_hours_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="@string/filter_hours_hint"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="number"
|
||||
android:minHeight="48dp"
|
||||
app:endIconMode="clear_text"
|
||||
app:layout_constraintBottom_toTopOf="@+id/filter_elev_text"
|
||||
app:layout_constraintEnd_toEndOf="@+id/filter_hours_text"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/filter_hours_text"
|
||||
app:layout_constraintTop_toBottomOf="@+id/filter_hours_text">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/filter_hours_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:inputType="textUri" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/filter_elev_text"
|
||||
style="@style/DialogText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/filter_elev_text"
|
||||
app:layout_constraintBottom_toTopOf="@+id/filter_elev_layout"
|
||||
app:layout_constraintEnd_toEndOf="@+id/filter_hours_text"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/filter_hours_text"
|
||||
app:layout_constraintTop_toBottomOf="@+id/filter_hours_layout" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/filter_elev_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="@string/filter_elev_hint"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="number|numberDecimal"
|
||||
android:minHeight="48dp"
|
||||
app:endIconMode="clear_text"
|
||||
app:layout_constraintBottom_toTopOf="@+id/filter_btn_neg"
|
||||
app:layout_constraintEnd_toEndOf="@+id/filter_hours_layout"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/filter_hours_layout"
|
||||
app:layout_constraintTop_toBottomOf="@+id/filter_elev_text">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/filter_elev_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:inputType="textUri" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/filter_btn_neg"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:text="@string/btn_cancel"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/filter_btn_pos"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/filter_elev_layout" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/filter_btn_pos"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:text="@string/btn_accept"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/filter_btn_neg"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/filter_btn_neg" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
@@ -1,94 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/SurfaceCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/locator_title"
|
||||
style="@style/DialogTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:minHeight="48dp"
|
||||
android:text="@string/locator_title"
|
||||
app:layout_constraintBottom_toTopOf="@+id/locator_text"
|
||||
app:layout_constraintEnd_toEndOf="@+id/locator_btn_pos"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/locator_btn_neg"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/locator_text"
|
||||
style="@style/DialogText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/locator_text"
|
||||
app:layout_constraintBottom_toTopOf="@+id/locator_layout"
|
||||
app:layout_constraintEnd_toEndOf="@+id/locator_title"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/locator_title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/locator_title" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/locator_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="@string/locator_hint"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="number"
|
||||
android:minHeight="48dp"
|
||||
app:endIconMode="clear_text"
|
||||
app:layout_constraintBottom_toTopOf="@+id/locator_btn_neg"
|
||||
app:layout_constraintEnd_toEndOf="@+id/locator_text"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/locator_text"
|
||||
app:layout_constraintTop_toBottomOf="@+id/locator_text">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/locator_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:inputType="textUri" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/locator_btn_neg"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:text="@string/btn_cancel"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/locator_btn_pos"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/locator_layout" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/locator_btn_pos"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:text="@string/btn_accept"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/locator_btn_neg"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/locator_btn_neg" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
@@ -1,66 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/SurfaceCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/modes_title"
|
||||
style="@style/DialogTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/modes_title"
|
||||
android:minHeight="48dp"
|
||||
app:layout_constraintEnd_toEndOf="@+id/modes_btn_pos"
|
||||
app:layout_constraintStart_toStartOf="@+id/modes_btn_neg"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/modes_recycler"
|
||||
style="@style/Recycler"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/modes_btn_neg"
|
||||
app:layout_constraintEnd_toEndOf="@+id/modes_btn_pos"
|
||||
app:layout_constraintStart_toStartOf="@+id/modes_btn_neg"
|
||||
app:layout_constraintTop_toBottomOf="@+id/modes_title" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/modes_btn_neg"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:text="@string/btn_cancel"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/modes_btn_pos"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/modes_btn_pos"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:text="@string/btn_accept"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/modes_btn_neg"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/modes_btn_neg" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
@@ -1,133 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/SurfaceCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/position_title"
|
||||
style="@style/DialogTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:minHeight="48dp"
|
||||
android:text="@string/position_title"
|
||||
app:layout_constraintBottom_toTopOf="@+id/position_lat_text"
|
||||
app:layout_constraintEnd_toEndOf="@+id/position_btn_pos"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/position_btn_neg"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/position_lat_text"
|
||||
style="@style/DialogText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/position_lat_text"
|
||||
app:layout_constraintBottom_toTopOf="@+id/position_lat_layout"
|
||||
app:layout_constraintEnd_toEndOf="@+id/position_title"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/position_title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/position_title" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/position_lat_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="@string/position_lat_hint"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="number"
|
||||
android:minHeight="48dp"
|
||||
app:endIconMode="clear_text"
|
||||
app:layout_constraintBottom_toTopOf="@+id/position_lon_text"
|
||||
app:layout_constraintEnd_toEndOf="@+id/position_lat_text"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/position_lat_text"
|
||||
app:layout_constraintTop_toBottomOf="@+id/position_lat_text">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/position_lat_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:digits="0123456789.-"
|
||||
android:inputType="number" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/position_lon_text"
|
||||
style="@style/DialogText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/position_lon_text"
|
||||
app:layout_constraintBottom_toTopOf="@+id/position_lon_layout"
|
||||
app:layout_constraintEnd_toEndOf="@+id/position_lat_text"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/position_lat_text"
|
||||
app:layout_constraintTop_toBottomOf="@+id/position_lat_layout" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/position_lon_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="@string/position_lon_hint"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="number|numberDecimal"
|
||||
android:minHeight="48dp"
|
||||
app:endIconMode="clear_text"
|
||||
app:layout_constraintBottom_toTopOf="@+id/position_btn_neg"
|
||||
app:layout_constraintEnd_toEndOf="@+id/position_lat_layout"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/position_lat_layout"
|
||||
app:layout_constraintTop_toBottomOf="@+id/position_lon_text">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/position_lon_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:digits="0123456789.-"
|
||||
android:inputType="number" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/position_btn_neg"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:text="@string/btn_cancel"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/position_btn_pos"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/position_lon_layout" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/position_btn_pos"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:text="@string/btn_accept"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/position_btn_neg"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/position_btn_neg" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
@@ -1,84 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/SurfaceCard"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sources_title"
|
||||
style="@style/DialogTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="22dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/sources_title"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/sources_btn_add"
|
||||
app:layout_constraintEnd_toStartOf="@+id/sources_btn_add"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toStartOf="@+id/sources_btn_neg" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/sources_btn_add"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/btn_add"
|
||||
app:layout_constraintEnd_toEndOf="@+id/sources_btn_pos"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/sources_title"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/sources_recycler"
|
||||
style="@style/Recycler"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/sources_btn_pos"
|
||||
app:layout_constraintDimensionRatio="w,1:1"
|
||||
app:layout_constraintEnd_toEndOf="@+id/sources_btn_pos"
|
||||
app:layout_constraintStart_toStartOf="@+id/sources_btn_neg"
|
||||
app:layout_constraintTop_toBottomOf="@+id/sources_btn_add" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/sources_btn_neg"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:text="@string/btn_cancel"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/sources_btn_pos"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/sources_btn_pos"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:text="@string/btn_accept"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/sources_btn_neg"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/sources_btn_neg" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
@@ -1,126 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:transitionGroup="true">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/entries_toolbar"
|
||||
style="@style/Toolbar">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/entries_btn_back"
|
||||
style="@style/ToolbarButton"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:contentDescription="@string/btn_back"
|
||||
android:src="@drawable/ic_back" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/entries_search"
|
||||
style="@style/ToolbarTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="52dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="52dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:hint="@string/entries_search_hint"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="text" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/entries_btn_modes"
|
||||
style="@style/ToolbarButton"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:contentDescription="@string/btn_modes"
|
||||
android:src="@drawable/ic_modes" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/entries_type_card"
|
||||
style="@style/SurfaceCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="42dp"
|
||||
android:layout_marginStart="@dimen/view_default_margin"
|
||||
android:layout_marginTop="60dp"
|
||||
android:layout_marginEnd="@dimen/view_default_margin">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/entries_type_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:paddingStart="14dp"
|
||||
android:paddingEnd="14dp"
|
||||
android:text="@string/types_title"
|
||||
android:textColor="@color/accent"
|
||||
android:textSize="@dimen/text_size_mediumLarge" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/entries_type_img"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:contentDescription="@string/types_title"
|
||||
android:paddingStart="14dp"
|
||||
android:paddingEnd="14dp"
|
||||
android:src="@drawable/ic_next"
|
||||
app:tint="@color/accent" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
style="@style/SurfaceCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="@dimen/view_default_margin"
|
||||
android:layout_marginTop="108dp"
|
||||
android:layout_marginEnd="@dimen/view_default_margin"
|
||||
android:layout_marginBottom="@dimen/surface_margin_bot">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/entries_recycler"
|
||||
style="@style/Recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/entries_progress"
|
||||
style="@style/RecyclerProgress" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<com.google.android.material.bottomappbar.BottomAppBar
|
||||
android:id="@+id/entries_bottom_bar"
|
||||
style="@style/BottomBar">
|
||||
|
||||
<Button
|
||||
android:id="@+id/entries_btn_select"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="@dimen/button_width_max"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:layout_marginStart="@dimen/button_margin_side"
|
||||
android:text="@string/btn_select" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/entries_btn_clear"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="@dimen/button_width_max"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:layout_marginEnd="@dimen/button_margin_side"
|
||||
android:text="@string/btn_clear_all" />
|
||||
|
||||
</com.google.android.material.bottomappbar.BottomAppBar>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/entries_fab"
|
||||
style="@style/FloatingActionButton"
|
||||
android:contentDescription="@string/btn_done"
|
||||
android:src="@drawable/ic_done"
|
||||
app:layout_anchor="@id/entries_bottom_bar" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,242 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:transitionGroup="true"
|
||||
android:keepScreenOn="true">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/map_toolbar"
|
||||
style="@style/Toolbar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/map_btn_back"
|
||||
style="@style/ToolbarButton"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:contentDescription="@string/btn_back"
|
||||
android:src="@drawable/ic_back" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/map_timer"
|
||||
style="@style/ToolbarTimer" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/map_card"
|
||||
style="@style/SurfaceCard"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="@dimen/view_default_margin"
|
||||
android:layout_marginTop="@dimen/view_default_margin"
|
||||
android:layout_marginEnd="@dimen/view_default_margin"
|
||||
android:layout_marginBottom="@dimen/view_default_margin"
|
||||
app:layout_constraintBottom_toTopOf="@+id/map_card_info"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/map_toolbar">
|
||||
|
||||
<org.osmdroid.views.MapView
|
||||
android:id="@+id/map_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/map_copyright"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:text="@string/map_copyright"
|
||||
android:textColor="@color/textMain"
|
||||
android:textSize="@dimen/text_size_copyright"
|
||||
app:layout_constraintEnd_toEndOf="@+id/map_card"
|
||||
app:layout_constraintStart_toStartOf="@+id/map_card"
|
||||
app:layout_constraintTop_toTopOf="@+id/map_card" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/map_card_info"
|
||||
style="@style/SurfaceCard"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/view_default_margin"
|
||||
android:layout_marginEnd="@dimen/view_default_margin"
|
||||
android:layout_marginBottom="@dimen/view_default_margin"
|
||||
app:layout_constraintBottom_toTopOf="@+id/map_coordinator"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/map_azimuth"
|
||||
style="@style/WorldMapText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/map_azimuth"
|
||||
app:layout_constraintBottom_toTopOf="@+id/map_data_alt"
|
||||
app:layout_constraintStart_toStartOf="@+id/map_data_period"
|
||||
app:layout_constraintTop_toBottomOf="@+id/map_data_period" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/map_data_lat"
|
||||
style="@style/WorldMapText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/map_latitude"
|
||||
app:layout_constraintBottom_toTopOf="@+id/map_data_qth"
|
||||
app:layout_constraintStart_toStartOf="@+id/map_data_alt"
|
||||
app:layout_constraintTop_toBottomOf="@+id/map_data_alt" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/map_data_lon"
|
||||
style="@style/WorldMapText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/map_longitude"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/map_data_lat"
|
||||
app:layout_constraintEnd_toEndOf="@+id/map_data_dst" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/map_data_alt"
|
||||
style="@style/WorldMapText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/map_altitude"
|
||||
app:layout_constraintBottom_toTopOf="@+id/map_data_lat"
|
||||
app:layout_constraintStart_toStartOf="@+id/map_azimuth"
|
||||
app:layout_constraintTop_toBottomOf="@+id/map_azimuth" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/map_data_dst"
|
||||
style="@style/WorldMapText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/map_distance"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/map_data_alt"
|
||||
app:layout_constraintEnd_toEndOf="@+id/map_elevation" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/map_elevation"
|
||||
style="@style/WorldMapText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/map_elevation"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/map_azimuth"
|
||||
app:layout_constraintEnd_toEndOf="@+id/map_data_phase" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/map_data_period"
|
||||
style="@style/WorldMapText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:text="@string/map_period"
|
||||
app:layout_constraintBottom_toTopOf="@+id/map_azimuth"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/map_data_phase"
|
||||
style="@style/WorldMapText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:text="@string/map_phase"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/map_data_period"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/map_data_qth"
|
||||
style="@style/WorldMapText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/map_qth"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/map_data_lat"
|
||||
app:layout_constraintTop_toBottomOf="@+id/map_data_lat" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/map_data_vel"
|
||||
style="@style/WorldMapText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/map_velocity"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/map_data_qth"
|
||||
app:layout_constraintEnd_toEndOf="@+id/map_data_lon" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/map_coordinator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<com.google.android.material.bottomappbar.BottomAppBar
|
||||
android:id="@+id/map_bottom_bar"
|
||||
style="@style/BottomBar" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/map_btn_prev"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="@dimen/button_width_max"
|
||||
android:layout_marginStart="@dimen/button_margin_side"
|
||||
android:text="@string/btn_prev"
|
||||
app:icon="@drawable/ic_prev"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/map_coordinator"
|
||||
app:layout_constraintStart_toStartOf="@+id/map_coordinator"
|
||||
app:layout_constraintTop_toTopOf="@+id/map_coordinator" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/map_data_visibility"
|
||||
style="@style/WorldMapText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_horizontal"
|
||||
android:maxLines="1"
|
||||
android:text="@string/map_visible"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/map_btn_prev"
|
||||
app:layout_constraintEnd_toStartOf="@+id/map_btn_next"
|
||||
app:layout_constraintStart_toEndOf="@+id/map_btn_prev" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/map_btn_next"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="@dimen/button_width_max"
|
||||
android:layout_marginEnd="@dimen/button_margin_side"
|
||||
android:text="@string/btn_next"
|
||||
app:icon="@drawable/ic_next"
|
||||
app:iconGravity="end"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/map_btn_prev"
|
||||
app:layout_constraintEnd_toEndOf="@+id/map_coordinator" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -1,116 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:transitionGroup="true">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/passes_toolbar"
|
||||
style="@style/Toolbar">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/passes_btn_refresh"
|
||||
style="@style/ToolbarButton"
|
||||
android:rotation="45"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:contentDescription="@string/btn_refresh"
|
||||
android:src="@drawable/ic_refresh" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/passes_timer"
|
||||
style="@style/ToolbarTimer" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/passes_btn_map"
|
||||
style="@style/ToolbarButton"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:contentDescription="@string/btn_map"
|
||||
android:src="@drawable/ic_map" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
style="@style/SurfaceCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="@dimen/view_default_margin"
|
||||
android:layout_marginTop="@dimen/surface_margin_top"
|
||||
android:layout_marginEnd="@dimen/view_default_margin"
|
||||
android:layout_marginBottom="@dimen/surface_margin_bot">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/passes_recycler"
|
||||
style="@style/Recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/passes_progress"
|
||||
style="@style/RecyclerProgress" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/passes_empty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="48dp"
|
||||
android:layout_marginEnd="48dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/passes_empty_img"
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="160dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:contentDescription="@string/passes_error"
|
||||
android:src="@drawable/ic_list"
|
||||
app:tint="@color/textDisabled" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/passes_empty_msg"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:gravity="center"
|
||||
android:text="@string/passes_error"
|
||||
android:textSize="@dimen/text_size_mediumLarge" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<com.google.android.material.bottomappbar.BottomAppBar
|
||||
android:id="@+id/passes_bottomBar"
|
||||
style="@style/BottomBar">
|
||||
|
||||
<Button
|
||||
android:id="@+id/passes_btn_filter"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="@dimen/button_width_max"
|
||||
android:layout_marginStart="@dimen/button_margin_side"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:text="@string/btn_filter" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/passes_btn_settings"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="@dimen/button_width_max"
|
||||
android:layout_marginEnd="@dimen/button_margin_side"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:text="@string/btn_settings" />
|
||||
|
||||
</com.google.android.material.bottomappbar.BottomAppBar>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/passes_fab"
|
||||
style="@style/FloatingActionButton"
|
||||
android:src="@drawable/ic_satellite"
|
||||
android:contentDescription="@string/btn_satellites"
|
||||
app:layout_anchor="@id/passes_bottomBar" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,235 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:transitionGroup="true"
|
||||
android:keepScreenOn="true">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/radar_toolbar"
|
||||
style="@style/Toolbar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/radar_btn_back"
|
||||
style="@style/ToolbarButton"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:contentDescription="@string/btn_back"
|
||||
android:src="@drawable/ic_back" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/radar_timer"
|
||||
style="@style/ToolbarTimer" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/radar_btn_map"
|
||||
style="@style/ToolbarButton"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:contentDescription="@string/btn_map"
|
||||
android:src="@drawable/ic_map" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/radar_card"
|
||||
style="@style/SurfaceCard"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="@dimen/view_default_margin"
|
||||
android:layout_marginTop="@dimen/view_default_margin"
|
||||
android:layout_marginEnd="@dimen/view_default_margin"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/radar_toolbar" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/radar_az_value"
|
||||
style="@style/RadarValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/radar_az_value"
|
||||
app:layout_constraintStart_toStartOf="@+id/radar_card"
|
||||
app:layout_constraintTop_toTopOf="@+id/radar_card" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/radar_az"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/radar_az_text"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
app:layout_constraintStart_toStartOf="@+id/radar_az_value"
|
||||
app:layout_constraintTop_toBottomOf="@+id/radar_az_value" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/radar_el_value"
|
||||
style="@style/RadarValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/radar_el_value"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/radar_az_value"
|
||||
app:layout_constraintEnd_toEndOf="@+id/radar_card" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/radar_el"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/radar_el_text"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/radar_az"
|
||||
app:layout_constraintEnd_toEndOf="@+id/radar_el_value" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/radar_alt_value"
|
||||
style="@style/RadarValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="@string/radar_alt_value"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/radar_card"
|
||||
app:layout_constraintStart_toStartOf="@+id/radar_az_value" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/radar_alt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/radar_alt_text"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
app:layout_constraintBottom_toTopOf="@+id/radar_alt_value"
|
||||
app:layout_constraintStart_toStartOf="@+id/radar_alt_value" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/radar_dst_value"
|
||||
style="@style/RadarValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/radar_dist_value"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/radar_alt_value"
|
||||
app:layout_constraintEnd_toEndOf="@+id/radar_el_value" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/radar_dst"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/radar_dist_text"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/radar_alt"
|
||||
app:layout_constraintEnd_toEndOf="@+id/radar_dst_value" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/radar_list_card"
|
||||
style="@style/SurfaceCard"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="@dimen/view_default_margin"
|
||||
android:layout_marginTop="@dimen/view_default_margin"
|
||||
android:layout_marginEnd="@dimen/view_default_margin"
|
||||
android:layout_marginBottom="@dimen/view_default_margin"
|
||||
app:layout_constraintBottom_toTopOf="@+id/radar_coordinator"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/radar_card">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/radar_recycler"
|
||||
style="@style/Recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/radar_progress"
|
||||
style="@style/RecyclerProgress" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/radar_empty_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="invisible"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/radar_empty_img"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:contentDescription="@string/radio_no_data"
|
||||
android:src="@drawable/ic_modes" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/radar_empty_msg"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:text="@string/radio_no_data"
|
||||
android:textSize="@dimen/text_size_mediumLarge" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/radar_coordinator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<com.google.android.material.bottomappbar.BottomAppBar
|
||||
android:id="@+id/radar_bottom_bar"
|
||||
style="@style/BottomBar" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/radar_btn_notify"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_marginStart="@dimen/button_margin_side"
|
||||
android:text="@string/btn_notify"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/radar_coordinator"
|
||||
app:layout_constraintEnd_toStartOf="@+id/radar_visibility"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/radar_coordinator"
|
||||
app:layout_constraintStart_toStartOf="@+id/radar_coordinator"
|
||||
app:layout_constraintTop_toTopOf="@+id/radar_coordinator" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/radar_visibility"
|
||||
style="@style/WorldMapText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_horizontal"
|
||||
android:maxLines="1"
|
||||
android:text="@string/radar_visible"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/radar_btn_notify"
|
||||
app:layout_constraintEnd_toStartOf="@+id/radar_btn_settings"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/radar_btn_notify" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/radar_btn_settings"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_marginEnd="@dimen/button_margin_side"
|
||||
android:text="@string/btn_settings"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/radar_btn_notify"
|
||||
app:layout_constraintEnd_toEndOf="@+id/radar_coordinator"
|
||||
app:layout_constraintStart_toEndOf="@+id/radar_visibility" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -1,151 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:transitionGroup="true">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/settings_toolbar"
|
||||
style="@style/Toolbar">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/settings_btn_back"
|
||||
style="@style/ToolbarButton"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:contentDescription="@string/btn_back"
|
||||
android:src="@drawable/ic_back" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/settings_title"
|
||||
style="@style/ToolbarTitle"
|
||||
android:text="@string/btn_settings" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
style="@style/Toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="@dimen/view_default_margin"
|
||||
android:layout_marginTop="@dimen/surface_margin_top"
|
||||
android:layout_marginEnd="@dimen/view_default_margin"
|
||||
android:layout_marginBottom="@dimen/surface_margin_bot"
|
||||
android:backgroundTint="@color/background">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/settings_scroll"
|
||||
style="@style/Recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<include
|
||||
android:id="@+id/settings_about"
|
||||
layout="@layout/card_about"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<include
|
||||
android:id="@+id/settings_location"
|
||||
layout="@layout/card_location"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/view_default_margin"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/settings_about" />
|
||||
|
||||
<include
|
||||
android:id="@+id/settings_data"
|
||||
layout="@layout/card_data"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/view_default_margin"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/settings_location" />
|
||||
|
||||
<include
|
||||
android:id="@+id/settings_remote"
|
||||
layout="@layout/card_remote"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/view_default_margin"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/settings_data" />
|
||||
|
||||
<include
|
||||
android:id="@+id/settings_btremote"
|
||||
layout="@layout/card_btremote"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/view_default_margin"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/settings_remote" />
|
||||
|
||||
<include
|
||||
android:id="@+id/settings_other"
|
||||
layout="@layout/card_other"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/view_default_margin"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/settings_btremote" />
|
||||
|
||||
<include
|
||||
android:id="@+id/settings_outro"
|
||||
layout="@layout/card_outro"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/view_default_margin"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/settings_other" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<com.google.android.material.bottomappbar.BottomAppBar
|
||||
android:id="@+id/settings_bottom_bar"
|
||||
style="@style/BottomBar">
|
||||
|
||||
<Button
|
||||
android:id="@+id/settings_btn_github"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="@dimen/button_width_max"
|
||||
android:layout_marginStart="@dimen/button_margin_side"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:text="@string/btn_github" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/settings_btn_fdroid"
|
||||
style="@style/NormalButton"
|
||||
android:layout_width="@dimen/button_width_max"
|
||||
android:layout_marginEnd="@dimen/button_margin_side"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:text="@string/btn_fdroid" />
|
||||
|
||||
</com.google.android.material.bottomappbar.BottomAppBar>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/settings_fab"
|
||||
style="@style/FloatingActionButton"
|
||||
android:src="@drawable/ic_support"
|
||||
android:contentDescription="@string/btn_support"
|
||||
app:layout_anchor="@id/settings_bottom_bar" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
style="@style/RecyclerItemCard">
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/entry_checkbox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="41dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:background="@color/cardRegular"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:padding="4dp"
|
||||
android:textColor="@color/textMain"
|
||||
android:textSize="@dimen/text_size_medium" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
style="@style/RecyclerItemCard">
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/mode_checkbox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="41dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:background="@color/cardRegular"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:padding="4dp"
|
||||
android:textColor="@color/textMain"
|
||||
android:textSize="@dimen/text_size_medium" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
@@ -1,115 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/RecyclerItemCard">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/pass_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pass_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/pass_satName"
|
||||
android:textColor="@color/accent"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pass_id"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/pass_satId"
|
||||
android:textColor="@color/accent"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/pass_name"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pass_aos"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/pass_aosAz"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
app:layout_constraintEnd_toStartOf="@+id/pass_los"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toStartOf="@+id/pass_name"
|
||||
app:layout_constraintTop_toBottomOf="@+id/pass_name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pass_altitude"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pass_altitude"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/pass_aos"
|
||||
app:layout_constraintEnd_toEndOf="@+id/pass_los"
|
||||
app:layout_constraintStart_toStartOf="@+id/pass_aos" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pass_los"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pass_los"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/pass_aos"
|
||||
app:layout_constraintEnd_toEndOf="@+id/pass_id"
|
||||
app:layout_constraintStart_toEndOf="@+id/pass_aos" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pass_start"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/pass_startTime"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
app:layout_constraintEnd_toStartOf="@+id/pass_end"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toStartOf="@+id/pass_aos"
|
||||
app:layout_constraintTop_toBottomOf="@+id/pass_aos" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pass_elev"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pass_elevation"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/pass_start"
|
||||
app:layout_constraintEnd_toEndOf="@+id/pass_end"
|
||||
app:layout_constraintStart_toStartOf="@+id/pass_start" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pass_end"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pass_endTime"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/pass_start"
|
||||
app:layout_constraintEnd_toEndOf="@+id/pass_los"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/pass_start" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/pass_progress"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="1dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@+id/pass_end"
|
||||
app:layout_constraintStart_toStartOf="@+id/pass_start"
|
||||
app:layout_constraintTop_toBottomOf="@+id/pass_elev" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
@@ -1,103 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/RecyclerItemCard">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/radio_guide"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.5" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/radio_info"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:maxLines="1"
|
||||
android:text="@string/radio_info"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
app:layout_constraintEnd_toStartOf="@+id/radio_up_img"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toEndOf="@+id/radio_down_img"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/radio_down_img"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:contentDescription="@string/radio_downlink"
|
||||
android:rotation="90"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/radio_info"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/radio_info"
|
||||
app:srcCompat="@drawable/ic_next" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/radio_downlink"
|
||||
style="@style/RadioFreq"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginTop="3dp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/radio_guide"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/radio_info" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/radio_up_img"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:contentDescription="@string/radio_uplink"
|
||||
android:rotation="270"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/radio_info"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/radio_info"
|
||||
app:srcCompat="@drawable/ic_next" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/radio_uplink"
|
||||
style="@style/RadioFreq"
|
||||
android:layout_marginEnd="4dp"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/radio_downlink"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/radio_guide" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/radio_mode"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="@string/radio_mode"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/radio_inverted"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/radio_downlink" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/radio_inverted"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/radio_inverted"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/radio_mode"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/radio_mode" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.textfield.TextInputLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/source_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:hint="@string/sources_hint"
|
||||
app:endIconDrawable="@drawable/ic_delete"
|
||||
app:endIconMode="custom">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/source_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:inputType="textUri" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_bg" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_fg" />
|
||||
</adaptive-icon>
|
||||
Loaded 100 of 5937 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user