mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Compare commits
440 commits
v2.7.14-in
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f21d8af9ae | ||
|
|
a90cb2d89e | ||
|
|
7492a33cf8 | ||
|
|
2b47da3b61 | ||
|
|
3322257cfd | ||
|
|
99e7407a90 | ||
|
|
9dd57725f2 | ||
|
|
2c1984ace5 | ||
|
|
94856d257f | ||
|
|
84fe24467f | ||
|
|
68a414b75b | ||
|
|
4257e7b7e4 | ||
|
|
14e86b90f1 | ||
|
|
ef0e159abb | ||
|
|
61d7f6fef3 | ||
|
|
a273dc6623 | ||
|
|
c866f60b59 | ||
|
|
10bc58d417 | ||
|
|
dd74e501f3 | ||
|
|
56cbc3670d | ||
|
|
15a7c19b74 | ||
|
|
b979663e24 | ||
|
|
9f3fe865e3 | ||
|
|
90f6e21a9c | ||
|
|
cdeb1ac532 | ||
|
|
adfe3bfed1 | ||
|
|
a97f704300 | ||
|
|
df3b5365f9 | ||
|
|
a6a889430b | ||
|
|
65b885a073 | ||
|
|
17e69c6d4c | ||
|
|
872c566ef1 | ||
|
|
3a2f2fc56b | ||
|
|
50896d455b | ||
|
|
a580cd0467 | ||
|
|
8e5d99410c | ||
|
|
0f900fe7d7 | ||
|
|
9ac02cf851 | ||
|
|
878905aea3 | ||
|
|
dea364dd17 | ||
|
|
c7d2a76851 | ||
|
|
f72b91328d | ||
|
|
d0057752f6 | ||
|
|
84621acb04 | ||
|
|
96419f3251 | ||
|
|
60ff495037 | ||
|
|
401f59489a | ||
|
|
a2763bdfeb | ||
|
|
72b981f73b | ||
|
|
50ade01e55 | ||
|
|
79ed0a865a | ||
|
|
bf0deef708 | ||
|
|
fa63a4ac50 | ||
|
|
f48fc61729 | ||
|
|
099aea2d81 | ||
|
|
c6f58cc799 | ||
|
|
27055290e2 | ||
|
|
3aadd29e67 | ||
|
|
9acdf5309f | ||
|
|
99378c9291 | ||
|
|
3c7e1266f8 | ||
|
|
743851b0b5 | ||
|
|
e46a8296cb | ||
|
|
27367e9064 | ||
|
|
28be6933c8 | ||
|
|
92166f0fa2 | ||
|
|
8e7c4f54a3 | ||
|
|
938a951737 | ||
|
|
76386e419c | ||
|
|
b13f9bf989 | ||
|
|
8a06157ff4 | ||
|
|
75e2177da7 | ||
|
|
61f90352c4 | ||
|
|
087fbbfb45 | ||
|
|
048c74db13 | ||
|
|
39620d063b | ||
|
|
35bf1fded5 | ||
|
|
4dd591af25 | ||
|
|
a8cdec7f55 | ||
|
|
1e29fec469 | ||
|
|
b0c603c7ed | ||
|
|
e424d4d076 | ||
|
|
17d85c88c4 | ||
|
|
5c47256b3f | ||
|
|
a11dee42a7 | ||
|
|
4156acf297 | ||
|
|
c059f19cc6 | ||
|
|
ade314d503 | ||
|
|
bc44af1597 | ||
|
|
eeed780e51 | ||
|
|
d03e61af6f | ||
|
|
916eb51b94 | ||
|
|
7ca7179197 | ||
|
|
9281324be3 | ||
|
|
6da9f088a9 | ||
|
|
e85300531e | ||
|
|
962c619c4c | ||
|
|
19502cd1e0 | ||
|
|
9468bc6ebe | ||
|
|
5e44cbd3a9 | ||
|
|
62264b10c6 | ||
|
|
174315b21f | ||
|
|
172680fd46 | ||
|
|
a3c0a4832d | ||
|
|
5f0e60eb21 | ||
|
|
40ea45a4fe | ||
|
|
1fe3f4423d | ||
|
|
0441093ce8 | ||
|
|
b3d0c97206 | ||
|
|
1f88a26d51 | ||
|
|
6b77658cb1 | ||
|
|
3794c79dae | ||
|
|
a6423d0a0f | ||
|
|
37e9e2c8f0 | ||
|
|
9c8532f80d | ||
|
|
929e273978 | ||
|
|
3d139d32fd | ||
|
|
ce32e640de | ||
|
|
77e30b60e1 | ||
|
|
520fa717a9 | ||
|
|
56332f4d77 | ||
|
|
02f6fd67b8 | ||
|
|
e70dabe94d | ||
|
|
eec27cf6f7 | ||
|
|
7ef382cce7 | ||
|
|
3d51a48da2 | ||
|
|
6f5fa49b94 | ||
|
|
ae5f021323 | ||
|
|
0355c7b8b3 | ||
|
|
1390a3cd4f | ||
|
|
93e0b9ca57 | ||
|
|
5c58709b0f | ||
|
|
decda75852 | ||
|
|
17e7c76583 | ||
|
|
aeef34f88c | ||
|
|
978ce19f93 | ||
|
|
1db4e03076 | ||
|
|
e23fab2667 | ||
|
|
ebf3b8272c | ||
|
|
dba037466e | ||
|
|
5e57efeb06 | ||
|
|
a1e94aa439 | ||
|
|
f07624be88 | ||
|
|
d5a9e32b32 | ||
|
|
9c0e9b82d6 | ||
|
|
e01c4abae7 | ||
|
|
20d934459a | ||
|
|
ad7003ed90 | ||
|
|
14b381c1eb | ||
|
|
537029a71c | ||
|
|
015ab5c0fb | ||
|
|
1649e46dd5 | ||
|
|
0576364c11 | ||
|
|
eb79421209 | ||
|
|
2ce110dffe | ||
|
|
750c4ea928 | ||
|
|
013a9afc96 | ||
|
|
975df02437 | ||
|
|
ad08a6c7b7 | ||
|
|
150ee3f1a4 | ||
|
|
60cc2f4237 | ||
|
|
cd9f1c0600 | ||
|
|
8dfb642deb | ||
|
|
87d507eb6e | ||
|
|
f817297ebe | ||
|
|
f33518de6d | ||
|
|
cc2fb45366 | ||
|
|
5f53bfa300 | ||
|
|
38a19e5599 | ||
|
|
547d349b48 | ||
|
|
15419aba6c | ||
|
|
72f4697d0d | ||
|
|
b3be9e2c38 | ||
|
|
d0e3b682ab | ||
|
|
6af3ad6f0c | ||
|
|
e111b61e4e | ||
|
|
1442e9354e | ||
|
|
5673eb90f3 | ||
|
|
53d21b4193 | ||
|
|
fda96e2f8c | ||
|
|
e468818c82 | ||
|
|
919da2904e | ||
|
|
9544df2bb9 | ||
|
|
db5403b436 | ||
|
|
fc86c696cd | ||
|
|
51251ab16a | ||
|
|
7e041c00e1 | ||
|
|
1fee6c4431 | ||
|
|
176340be5f | ||
|
|
05229fd652 | ||
|
|
0167063497 | ||
|
|
e249461e3c | ||
|
|
d1ca8ec527 | ||
|
|
fefe74d217 | ||
|
|
89547afe6b | ||
|
|
d8e295cafb | ||
|
|
7c9d007a1f | ||
|
|
464a12b9f7 | ||
|
|
1faa802fe6 | ||
|
|
6a5115b897 | ||
|
|
c75c9b34d6 | ||
|
|
ae4465d7c8 | ||
|
|
38848b4ea4 | ||
|
|
69f147a1d8 | ||
|
|
445ec27fa4 | ||
|
|
ad6e12445b | ||
|
|
f2d09ff79d | ||
|
|
3feec759a1 | ||
|
|
791601dec2 | ||
|
|
0bc7ca820b | ||
|
|
1c1c208d48 | ||
|
|
e106badec7 | ||
|
|
d979269552 | ||
|
|
141b54ff9c | ||
|
|
c259c76550 | ||
|
|
518096ddc8 | ||
|
|
4be0fb9c65 | ||
|
|
0d2c2e8a98 | ||
|
|
3b485a0109 | ||
|
|
e1fa05f63f | ||
|
|
ff6e1813ea | ||
|
|
da5b28443e | ||
|
|
7484cc69b4 | ||
|
|
36290fc94b | ||
|
|
6f95435cfc | ||
|
|
a005231d94 | ||
|
|
b608a04ca4 | ||
|
|
d62481a532 | ||
|
|
e81e84a544 | ||
|
|
eabab0b137 | ||
|
|
705f617adb | ||
|
|
c55d25bade | ||
|
|
f622fae74f | ||
|
|
c470b9a366 | ||
|
|
8ce17defb7 | ||
|
|
6516287c62 | ||
|
|
b0e91a390c | ||
|
|
553ca2f8ed | ||
|
|
9b8ac6a460 | ||
|
|
0c3ab92908 | ||
|
|
3a9f611fc0 | ||
|
|
cd328b236d | ||
|
|
b45bc9be90 | ||
|
|
41c82abc9e | ||
|
|
96060a0a4d | ||
|
|
664ebf218e | ||
|
|
f826cac6c8 | ||
|
|
a0b4c56505 | ||
|
|
57242d905c | ||
|
|
b4afe22030 | ||
|
|
82466be072 | ||
|
|
d2c9036d24 | ||
|
|
7b327215f3 | ||
|
|
b3b38acc0b | ||
|
|
acb328dae3 | ||
|
|
cb89b111de | ||
|
|
5a287f7133 | ||
|
|
ee146df334 | ||
|
|
d5d4aa4577 | ||
|
|
883ab5f7da | ||
|
|
55729c9e2d | ||
|
|
27bcbcb7be | ||
|
|
fa98d20256 | ||
|
|
c38bfc64de | ||
|
|
d136b162a4 | ||
|
|
f04924ded5 | ||
|
|
a331f96576 | ||
|
|
88d11aafec | ||
|
|
d61c0c9a67 | ||
|
|
3bd8669cbe | ||
|
|
6e50db0b91 | ||
|
|
b1e433e0dc | ||
|
|
05dc1703da | ||
|
|
c4087c2ab7 | ||
|
|
6cdd10d936 | ||
|
|
0a3d2ec4b9 | ||
|
|
398e751a87 | ||
|
|
00697cc3c1 | ||
|
|
e36176bbf7 | ||
|
|
89e38611d8 | ||
|
|
1e55e554be | ||
|
|
4eb711ce58 | ||
|
|
b7b973eaee | ||
|
|
bc08093f6c | ||
|
|
b982b145e6 | ||
|
|
a5d3914149 | ||
|
|
ad06eafff7 | ||
|
|
cdba140eeb | ||
|
|
06b9f8c77a | ||
|
|
1b0dc75dfe | ||
|
|
dcbbc0823b | ||
|
|
df3a094430 | ||
|
|
5158d6c9d6 | ||
|
|
04a71c2743 | ||
|
|
54b07d41de | ||
|
|
d314ee2d8a | ||
|
|
eae5a6bdac | ||
|
|
1e9e838025 | ||
|
|
3263b0614f | ||
|
|
59408ef46e | ||
|
|
06c990026f | ||
|
|
49a6a1d4a9 | ||
|
|
cb95cace25 | ||
|
|
3bbb8a65ba | ||
|
|
afa7552141 | ||
|
|
7d63f8b824 | ||
|
|
807db83f53 | ||
|
|
0d0bdf9172 | ||
|
|
0c3a841a80 | ||
|
|
190e62ce68 | ||
|
|
5eb6e501c0 | ||
|
|
212acaecac | ||
|
|
a10fe61d0f | ||
|
|
9ad28e924f | ||
|
|
8c964a15ca | ||
|
|
0b2e89c46f | ||
|
|
0e5f94579f | ||
|
|
6e81ceec91 | ||
|
|
80cae8e620 | ||
|
|
5edb8abd05 | ||
|
|
802aa09aab | ||
|
|
2c52977683 | ||
|
|
4e64182afd | ||
|
|
513dcc2f78 | ||
|
|
e29fd596b6 | ||
|
|
fae6f83968 | ||
|
|
5610cc3924 | ||
|
|
ac8119b086 | ||
|
|
609d24a9e4 | ||
|
|
b63192dccc | ||
|
|
a8044a2402 | ||
|
|
acf7aea098 | ||
|
|
bff87daaa7 | ||
|
|
365e278335 | ||
|
|
8c6892a4da | ||
|
|
832e785785 | ||
|
|
06f002a198 | ||
|
|
2bfd225b68 | ||
|
|
305466514a | ||
|
|
48740fe280 | ||
|
|
be70743ed6 | ||
|
|
427c0f3bbb | ||
|
|
90844301e8 | ||
|
|
07ec771758 | ||
|
|
f45993ede2 | ||
|
|
8bb1e86511 | ||
|
|
da11703ccd | ||
|
|
b0f1f93c5a | ||
|
|
5cc1e94a13 | ||
|
|
afe1356430 | ||
|
|
aacf5c69e9 | ||
|
|
0ed9b6633b | ||
|
|
eb3349fa11 | ||
|
|
20f358e01c | ||
|
|
87e291f58d | ||
|
|
3d93d0b4e3 | ||
|
|
84bb6d24e4 | ||
|
|
bdfd7b9251 | ||
|
|
3321c47200 | ||
|
|
629d80ec65 | ||
|
|
3957b0823c | ||
|
|
55cea44993 | ||
|
|
ac6bb5479b | ||
|
|
f4364cff9a | ||
|
|
3ccfcf644f | ||
|
|
6a1f3b197a | ||
|
|
c72e085f10 | ||
|
|
f70623db14 | ||
|
|
a562f274bf | ||
|
|
cfef01ccac | ||
|
|
a902da4ca0 | ||
|
|
7a1e1778f4 | ||
|
|
2ef0547fb2 | ||
|
|
d076361c55 | ||
|
|
b1070321fe | ||
|
|
e3e010e3db | ||
|
|
f86ba289d8 | ||
|
|
ee03b6d186 | ||
|
|
875cf1cff2 | ||
|
|
a5390a80e7 | ||
|
|
0ce322a0f5 | ||
|
|
182ad933f4 | ||
|
|
f663866d53 | ||
|
|
cffbd08806 | ||
|
|
f3775a601c | ||
|
|
27e7dec69e | ||
|
|
b9b68d2779 | ||
|
|
87fdaa26ff | ||
|
|
dfab02bfb4 | ||
|
|
a854c839e4 | ||
|
|
9d9f95961d | ||
|
|
79a4a3671f | ||
|
|
c9005432ea | ||
|
|
2e13b1ab17 | ||
|
|
43f9aa0b50 | ||
|
|
63984f0723 | ||
|
|
68b2b6d88e | ||
|
|
5a5aa1f026 | ||
|
|
c1309545ea | ||
|
|
af3f36b648 | ||
|
|
b0258d0cf1 | ||
|
|
6a1a612c38 | ||
|
|
5fc7e46c29 | ||
|
|
3e986032a5 | ||
|
|
1e0613d520 | ||
|
|
5b43dcb636 | ||
|
|
02e01bb331 | ||
|
|
bd9c730c25 | ||
|
|
6f393a56ec | ||
|
|
7812e7df72 | ||
|
|
7cdfff9ae8 | ||
|
|
7067806444 | ||
|
|
6a858acb4a | ||
|
|
744db2d5bd | ||
|
|
034c85d191 | ||
|
|
fe67219207 | ||
|
|
17dcbed6b1 | ||
|
|
8f055fda93 | ||
|
|
be8f756694 | ||
|
|
c234ace312 | ||
|
|
657553f830 | ||
|
|
05e2c5d457 | ||
|
|
9dc1319845 | ||
|
|
f4c2a37913 | ||
|
|
4d21278514 | ||
|
|
66d4cfb8c3 | ||
|
|
2c49db8041 | ||
|
|
40244f8337 | ||
|
|
0fc3fd280e | ||
|
|
bb37c66353 | ||
|
|
728c300315 | ||
|
|
8c6bd8ab7a | ||
|
|
5f31df96d8 | ||
|
|
9ba4d50e60 | ||
|
|
362ab6357c | ||
|
|
fdd07f893f | ||
|
|
b2b21e10e2 | ||
|
|
a07992530c | ||
|
|
225dc232b6 | ||
|
|
145c256972 |
1833 changed files with 95136 additions and 46129 deletions
27
.copilotignore
Normal file
27
.copilotignore
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Ignore build artifacts and generated files from Copilot indexing
|
||||
# This saves context window tokens and prevents Copilot from hallucinating off of minified code.
|
||||
|
||||
# Build directories
|
||||
**/build/**
|
||||
.gradle/
|
||||
.idea/
|
||||
|
||||
# Android generated files
|
||||
**/generated/**
|
||||
.cxx/
|
||||
.externalNativeBuild/
|
||||
|
||||
# Git history & worktrees
|
||||
.git/
|
||||
.worktrees/
|
||||
|
||||
# Protobuf (Prevents Copilot from suggesting raw protobuf byte buffers)
|
||||
core/proto/
|
||||
|
||||
# Environment and secrets
|
||||
local.properties
|
||||
secrets.properties
|
||||
*.jks
|
||||
|
||||
# Agent References (Prevents pollution of project space with external code)
|
||||
.agent_refs/
|
||||
5
.gemini/settings.json
Normal file
5
.gemini/settings.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"context": {
|
||||
"fileName": ["AGENTS.md", "GEMINI.md"]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
name: 'Calculate Version Code'
|
||||
description: 'Calculates the Android versionCode based on the Git commit count plus an offset.'
|
||||
outputs:
|
||||
versionCode:
|
||||
description: "The calculated version code"
|
||||
value: ${{ steps.calculate_version.outputs.VERSION_CODE }}
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Calculate Version Code
|
||||
id: calculate_version
|
||||
shell: bash
|
||||
run: |
|
||||
# This action assumes that the repo has been checked out with `fetch-depth: 0`
|
||||
GIT_COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
OFFSET=30630
|
||||
VERSION_CODE=$((GIT_COMMIT_COUNT + OFFSET))
|
||||
echo "Calculated versionCode: $VERSION_CODE (from $GIT_COMMIT_COUNT commits + $OFFSET offset)"
|
||||
echo "VERSION_CODE=$VERSION_CODE" >> $GITHUB_OUTPUT
|
||||
40
.github/actions/gradle-setup/action.yml
vendored
Normal file
40
.github/actions/gradle-setup/action.yml
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
name: Gradle Setup
|
||||
description: Setup Java and Gradle for KMP builds
|
||||
inputs:
|
||||
cache_read_only:
|
||||
description: 'Whether Gradle cache is read-only'
|
||||
default: 'true'
|
||||
jdk_distribution:
|
||||
description: 'JDK distribution (temurin or jetbrains)'
|
||||
default: 'temurin'
|
||||
gradle_encryption_key:
|
||||
description: 'Encryption key for Gradle remote cache'
|
||||
required: false
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Copy CI Gradle properties
|
||||
shell: bash
|
||||
run: mkdir -p ~/.gradle && cp .github/ci-gradle.properties ~/.gradle/gradle.properties
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/actions/wrapper-validation@v6
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: ${{ inputs.jdk_distribution }}
|
||||
token: ${{ github.token }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
cache-read-only: ${{ inputs.cache_read_only }}
|
||||
cache-encryption-key: ${{ inputs.gradle_encryption_key }}
|
||||
cache-cleanup: on-success
|
||||
add-job-summary: always
|
||||
gradle-home-cache-includes: |
|
||||
caches
|
||||
notifications
|
||||
~/.m2/repository/org/robolectric
|
||||
52
.github/ci-gradle.properties
vendored
Normal file
52
.github/ci-gradle.properties
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#
|
||||
# CI-specific Gradle properties.
|
||||
#
|
||||
# This file is copied to ~/.gradle/gradle.properties by the gradle-setup
|
||||
# composite action, overriding the dev-oriented values in the repo-root
|
||||
# gradle.properties. Inspired by the nowinandroid & sqldelight patterns.
|
||||
#
|
||||
|
||||
# ── Daemon ────────────────────────────────────────────────────────────
|
||||
# Single-use CI runners never reuse a daemon, so the startup cost is pure
|
||||
# overhead. Disabling it also avoids "daemon disappeared" warnings.
|
||||
org.gradle.daemon=false
|
||||
|
||||
# ── Memory ────────────────────────────────────────────────────────────
|
||||
# Standard GitHub runners have 7 GB RAM. Keep Gradle + Kotlin daemon
|
||||
# within budget (4g Gradle + 2g Kotlin daemon + 1g OS/tooling headroom).
|
||||
org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g -Dfile.encoding=UTF-8
|
||||
kotlin.daemon.jvm.options=-Xmx2g -XX:+UseParallelGC
|
||||
|
||||
# ── Parallelism ───────────────────────────────────────────────────────
|
||||
org.gradle.parallel=true
|
||||
org.gradle.workers.max=4
|
||||
|
||||
# ── Caching & Configuration ──────────────────────────────────────────
|
||||
org.gradle.caching=true
|
||||
org.gradle.configuration-cache=true
|
||||
org.gradle.configureondemand=false
|
||||
org.gradle.vfs.watch=false
|
||||
org.gradle.isolated-projects=true
|
||||
|
||||
# ── Kotlin ────────────────────────────────────────────────────────────
|
||||
# Incremental compilation is wasted on fresh CI checkouts (no prior build
|
||||
# state to diff against). Disabling avoids the overhead of maintaining
|
||||
# incremental state that will never be reused.
|
||||
kotlin.incremental=false
|
||||
kotlin.code.style=official
|
||||
kotlin.parallel.tasks.in.project=true
|
||||
|
||||
# ── KSP ──────────────────────────────────────────────────────────────
|
||||
# In CI, KSP incremental processing adds overhead without benefit (fresh
|
||||
# checkouts). Keep intermodule incremental off (no prior state).
|
||||
ksp.incremental=false
|
||||
ksp.run.in.process=true
|
||||
|
||||
# ── Android ──────────────────────────────────────────────────────────
|
||||
android.experimental.lint.analysisPerComponent=true
|
||||
# Disable unused build features to reduce build time
|
||||
android.defaults.buildfeatures.resvalues=false
|
||||
android.defaults.buildfeatures.shaders=false
|
||||
|
||||
# ── Misc ─────────────────────────────────────────────────────────────
|
||||
org.gradle.welcome=never
|
||||
27
.github/copilot-commit-message-instructions.md
vendored
Normal file
27
.github/copilot-commit-message-instructions.md
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# GitHub Copilot Commit Message Instructions
|
||||
|
||||
<role>
|
||||
You are an expert Git maintainer enforcing Conventional Commits.
|
||||
</role>
|
||||
|
||||
<instructions>
|
||||
1. **Format:** Use the Conventional Commits format: `<type>(<scope>): <subject>` (Replace angle brackets with actual text, do NOT output angle brackets).
|
||||
2. **Types allowed:**
|
||||
- `feat` (new feature for the user, not a new feature for build script)
|
||||
- `fix` (bug fix for the user, not a fix to a build script)
|
||||
- `docs` (changes to the documentation)
|
||||
- `style` (formatting, missing semi colons, etc; no production code change)
|
||||
- `refactor` (refactoring production code, e.g. KMP migration, extracting to commonMain)
|
||||
- `test` (adding missing tests, refactoring tests; no production code change)
|
||||
- `chore` (updating grunt tasks etc; no production code change)
|
||||
3. **Scope:** Use the module or logical component as the scope (e.g., `ui`, `navigation`, `ble`, `firmware`, `deps`, `ai`).
|
||||
4. **Subject line:**
|
||||
- Use the imperative, present tense: "change" not "changed" nor "changes".
|
||||
- Do not capitalize the first letter.
|
||||
- Do not use a period (.) at the end.
|
||||
- Keep it under 50 characters if possible.
|
||||
5. **Body (Optional but recommended for large diffs):**
|
||||
- Leave one blank line after the subject.
|
||||
- Explain *why* the change was made, not just *what* changed.
|
||||
- If migrating to KMP or extracting to `commonMain`, explicitly state "Decoupled from Android framework".
|
||||
</instructions>
|
||||
185
.github/copilot-instructions.md
vendored
185
.github/copilot-instructions.md
vendored
|
|
@ -1,183 +1,6 @@
|
|||
# Copilot Instructions for Meshtastic-Android
|
||||
# Meshtastic Android - GitHub Copilot Guide
|
||||
|
||||
## Repository Summary
|
||||
> **Note:** The canonical instructions for all AI Agents have been deduplicated.
|
||||
|
||||
Meshtastic-Android is a native Android client application for the Meshtastic mesh networking project. It enables users to communicate via off-grid, decentralized mesh networks using LoRa radios. The app is written in Kotlin and follows modern Android development practices.
|
||||
|
||||
**Key Repository Details:**
|
||||
- **Language:** Kotlin (primary), with some Java and AIDL files
|
||||
- **Build System:** Gradle with Kotlin DSL
|
||||
- **Size:** ~3MB source code across 3 modules
|
||||
- **Target Platform:** Android API 26+ (Android 8.0+), targeting API 36
|
||||
- **Architecture:** Modern Android with Jetpack Compose, Hilt DI, Room database
|
||||
- **Product Flavors:** `fdroid` (F-Droid) and `google` (Google Play Store)
|
||||
- **Build Types:** `debug` and `release`
|
||||
|
||||
## Essential Build & Test Commands
|
||||
|
||||
**ALWAYS run these commands in the exact order specified to avoid build failures:**
|
||||
|
||||
### Prerequisites Setup
|
||||
1. **JDK Requirement:** JDK 17 is required (compatible with most developer environments)
|
||||
2. **Secrets Configuration:** Copy `secrets.defaults.properties` to `local.properties` and update:
|
||||
```properties
|
||||
MAPS_API_KEY=your_google_maps_api_key_here
|
||||
datadogApplicationId=your_datadog_app_id
|
||||
datadogClientToken=your_datadog_client_token
|
||||
```
|
||||
3. **Clean Environment:** Always start with `./gradlew clean` for fresh builds
|
||||
|
||||
### Build Commands (Validated Working Order)
|
||||
```bash
|
||||
# 1. ALWAYS clean first for reliable builds
|
||||
./gradlew clean
|
||||
|
||||
# 2. Check code formatting (run before making changes)
|
||||
./gradlew spotlessCheck
|
||||
|
||||
# 3. Apply automatic code formatting fixes
|
||||
./gradlew spotlessApply
|
||||
|
||||
# 4. Run static code analysis/linting
|
||||
./gradlew detekt
|
||||
|
||||
# 5. Build debug APKs for both flavors (takes 3-5 minutes)
|
||||
./gradlew assembleDebug
|
||||
|
||||
# 6. Build specific flavor variants
|
||||
./gradlew assembleFdroidDebug # F-Droid debug build
|
||||
./gradlew assembleGoogleDebug # Google debug build
|
||||
./gradlew assembleFdroidRelease # F-Droid release build
|
||||
./gradlew assembleGoogleRelease # Google release build
|
||||
|
||||
# 7. Run local unit tests (takes 2-3 minutes)
|
||||
./gradlew test
|
||||
|
||||
# 8. Run specific flavor unit tests
|
||||
./gradlew testFdroidDebug
|
||||
./gradlew testGoogleDebug
|
||||
|
||||
# 9. Run instrumented tests (requires Android device/emulator, takes 5-10 minutes)
|
||||
./gradlew connectedAndroidTest
|
||||
|
||||
# 10. Run lint checks for both flavors
|
||||
./gradlew lintFdroidDebug lintGoogleDebug
|
||||
```
|
||||
|
||||
### Time Requirements
|
||||
- Clean build: 3-5 minutes
|
||||
- Unit tests: 2-3 minutes
|
||||
- Instrumented tests: 5-10 minutes
|
||||
- Detekt analysis: 1-2 minutes
|
||||
- Spotless formatting: 30 seconds
|
||||
|
||||
### Environment Setup
|
||||
**Required Tools:**
|
||||
- Android SDK API 36 (compile target)
|
||||
- JDK 17 (Preferred for consistency across project and plugins)
|
||||
- Gradle 9.0+ (downloaded automatically by wrapper)
|
||||
|
||||
**Optional but Recommended:**
|
||||
- Install pre-push Git hook: `./gradlew spotlessInstallGitPrePushHook --no-configuration-cache`
|
||||
|
||||
## Project Architecture & Layout
|
||||
|
||||
### Module Structure
|
||||
```
|
||||
├── app/ # Main Android application
|
||||
│ ├── src/main/ # Main source code
|
||||
│ ├── src/test/ # Unit tests
|
||||
│ ├── src/androidTest/ # Instrumented tests
|
||||
│ ├── src/fdroid/ # F-Droid specific code
|
||||
│ └── src/google/ # Google Play specific code
|
||||
├── core/ # Core library modules
|
||||
├── network/ # HTTP API networking library
|
||||
├── mesh_service_example/ # AIDL service usage example
|
||||
├── build-logic/ # Build configuration convention plugins
|
||||
└── config/ # Linting and formatting configs
|
||||
├── detekt/ # Detekt static analysis rules
|
||||
└── spotless/ # Code formatting configuration
|
||||
```
|
||||
|
||||
### Key Configuration Files
|
||||
- `config.properties` - Version constants and build config
|
||||
- `app/build.gradle.kts` - Main app build configuration
|
||||
- `config/detekt/detekt.yml` - Static analysis rules
|
||||
- `config/spotless/.editorconfig` - Code formatting rules
|
||||
- `gradle.properties` - Gradle build settings
|
||||
- `secrets.defaults.properties` - Template for secrets (copy to `local.properties`)
|
||||
|
||||
### Architecture Components
|
||||
- **UI Framework:** Jetpack Compose with Material 3
|
||||
- **State Management:** Unidirectional Data Flow with ViewModels
|
||||
- **Dependency Injection:** Hilt
|
||||
- **Navigation:** Jetpack Navigation Compose
|
||||
- **Local Data:** Room database + DataStore preferences
|
||||
- **Remote Data:** Custom Bluetooth/WiFi protocol + HTTP API (network module)
|
||||
- **Background Work:** WorkManager
|
||||
- **Communication:** AIDL service interface (`IMeshService.aidl`)
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
### GitHub Workflows (.github/workflows/)
|
||||
- **pull-request.yml** - Runs on every PR: build, detekt, tests
|
||||
- **reusable-android-build.yml** - Shared build logic: spotless, detekt, lint, assemble, test
|
||||
- **reusable-android-test.yml** - Instrumented tests on Android emulators (API 26, 35)
|
||||
|
||||
### CI Commands (Must Pass)
|
||||
```bash
|
||||
# Exact commands run in CI that must pass:
|
||||
./gradlew :app:spotlessCheck :app:detekt :app:lintFdroidDebug :app:lintGoogleDebug :app:assembleDebug :app:testFdroidDebug :app:testGoogleDebug --configuration-cache --scan
|
||||
./gradlew :app:connectedFdroidDebugAndroidTest :app:connectedGoogleDebugAndroidTest --configuration-cache --scan
|
||||
```
|
||||
|
||||
### Validation Steps
|
||||
1. **Code Style:** Spotless check (auto-fixable with `spotlessApply`)
|
||||
2. **Static Analysis:** Detekt with custom rules in `config/detekt/detekt.yml`
|
||||
3. **Lint Checks:** Android lint for both flavors
|
||||
4. **Unit Tests:** JUnit tests in `app/src/test/`
|
||||
5. **UI Tests:** Compose UI tests in `app/src/androidTest/`
|
||||
|
||||
## Common Issues & Solutions
|
||||
|
||||
### Build Failures
|
||||
- **Gradle version error:** Ensure JDK 17 (Compatible version)
|
||||
- **Missing secrets:** Copy `secrets.defaults.properties` → `local.properties`
|
||||
- **Configuration cache:** Add `--no-configuration-cache` flag if issues persist
|
||||
- **Clean state:** Always run `./gradlew clean` before debugging build issues
|
||||
|
||||
### Testing Issues
|
||||
- **Instrumented tests:** Require Android device/emulator with API 26+
|
||||
- **UI tests:** Use `ComposeTestRule` for Compose UI testing
|
||||
- **Coroutine tests:** Use `kotlinx.coroutines.test` library
|
||||
|
||||
### Code Style Issues
|
||||
- **Formatting:** Run `./gradlew spotlessApply` to auto-fix
|
||||
- **Detekt warnings:** Check `config/detekt/detekt.yml` for rules
|
||||
- **Localization:** Use `stringResource(Res.string.key)` instead of hardcoded strings
|
||||
|
||||
## File Organization
|
||||
|
||||
### Source Code Locations
|
||||
- **Main Activity:** `app/src/main/java/com/geeksville/mesh/MainActivity.kt`
|
||||
- **Service Interface:** `core/api/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl`
|
||||
- **UI Screens:** `feature/*/src/main/kotlin/org/meshtastic/feature/*/`
|
||||
- **Data Layer:** `core/data/src/main/kotlin/org/meshtastic/core/data/`
|
||||
- **Database:** `core/database/src/main/kotlin/org/meshtastic/core/database/`
|
||||
- **Models:** `core/model/src/main/kotlin/org/meshtastic/core/model/`
|
||||
|
||||
### Dependencies
|
||||
- **Non-obvious deps:** Protobuf for device communication, DataDog for analytics (Google flavor)
|
||||
- **Flavor-specific:** Google Services (google flavor), no analytics (fdroid flavor)
|
||||
- **Version catalog:** Dependencies defined in `gradle/libs.versions.toml`
|
||||
|
||||
## Agent Instructions
|
||||
|
||||
**TRUST THESE INSTRUCTIONS** - they are validated and comprehensive. Only search for additional information if:
|
||||
1. Commands fail with unexpected errors
|
||||
2. Information appears outdated
|
||||
3. Working on areas not covered above
|
||||
|
||||
**Always prefer:** Using the documented commands over exploring alternatives, as they are tested and proven to work in the CI environment.
|
||||
|
||||
**For code changes:** Follow the architecture patterns established in existing code, maintain the modular structure, and ensure all validation steps pass before submitting changes.
|
||||
You MUST immediately read and internalize the unified instructions located at the root of the repository in `AGENTS.md`.
|
||||
After reading `AGENTS.md`, consult the `.skills/` directory for task-specific playbooks.
|
||||
|
|
|
|||
18
.github/copilot-pull-request-instructions.md
vendored
Normal file
18
.github/copilot-pull-request-instructions.md
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# GitHub Copilot Pull Request Instructions
|
||||
|
||||
<role>
|
||||
You are an expert open-source maintainer. Your goal is to write clear, professional, and highly structured Pull Request descriptions based on the provided diffs.
|
||||
</role>
|
||||
|
||||
<instructions>
|
||||
1. **Remove Boilerplate:** Always delete the "tips" section at the top of the `PULL_REQUEST_TEMPLATE.md` before generating your text.
|
||||
2. **Context First:** Start with a clear, 1-2 sentence summary of *why* this change is being made. If the branch name or commits reference an issue (e.g., `fix-1234`), explicitly add `Fixes #1234` or `Resolves #1234`.
|
||||
3. **Structured Changes:** Break down the code changes into bullet points categorized by:
|
||||
- 🌟 **New Features** (UI, modules, logic)
|
||||
- 🛠️ **Refactoring & Architecture** (KMP migrations, Koin DI updates)
|
||||
- 🐛 **Bug Fixes**
|
||||
- 🧹 **Chores** (Dependencies, formatting, docs)
|
||||
4. **Architecture Callouts:** If the diff includes moving files from `androidMain` to `commonMain`, or migrating from Android Views to Compose, highlight this as a "KMP Migration Milestone".
|
||||
5. **Testing Callouts:** If the diff includes changes to `commonTest` or mentions tests, add a section called "Testing Performed" and list the tests that were added/modified.
|
||||
6. **No "Magic" Text:** Do not invent URLs or insert fake image placeholders. Leave the HTML comment block for images intact so the user can manually add their screenshots.
|
||||
</instructions>
|
||||
11
.github/instructions/android-source-set.instructions.md
vendored
Normal file
11
.github/instructions/android-source-set.instructions.md
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
applyTo: "**/androidMain/**/*.kt"
|
||||
---
|
||||
|
||||
# Android Source-Set Rules
|
||||
|
||||
- This is `androidMain` — Android framework imports (`android.*`, `java.*`) are allowed here.
|
||||
- Do NOT put business logic here. Business logic belongs in `commonMain`.
|
||||
- If you find identical pure-Kotlin logic in both `androidMain` and `jvmMain`, extract it to `commonMain`.
|
||||
- Use `expect`/`actual` only for small platform primitives. Prefer interfaces + DI.
|
||||
- Keep `expect` declarations in `FileIo.kt` and shared helpers in `FileIoUtils.kt` to avoid JVM duplicate class errors.
|
||||
10
.github/instructions/build-logic.instructions.md
vendored
Normal file
10
.github/instructions/build-logic.instructions.md
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
applyTo: "build-logic/**/*.kt"
|
||||
---
|
||||
|
||||
# Build-Logic Convention Plugin Rules
|
||||
|
||||
- Prefer lazy Gradle configuration (`configureEach`, `withPlugin`, provider APIs).
|
||||
- Avoid `afterEvaluate` unless there is no viable lazy alternative.
|
||||
- Check `gradle/libs.versions.toml` for version catalog aliases before adding new ones.
|
||||
- Convention plugins: `meshtastic.kmp.feature`, `meshtastic.kmp.library`, `meshtastic.kmp.jvm.android`, `meshtastic.koin`.
|
||||
14
.github/instructions/ci-workflows.instructions.md
vendored
Normal file
14
.github/instructions/ci-workflows.instructions.md
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
applyTo: "**/*.yml"
|
||||
excludeAgent: "code-review"
|
||||
---
|
||||
|
||||
# CI Workflow Rules
|
||||
|
||||
- Prefer explicit Gradle task paths (`app:lintFdroidDebug`) over shorthand (`lintDebug`).
|
||||
- CI uses `.github/ci-gradle.properties` — don't assume local `gradle.properties` values.
|
||||
- CI passes `-Pci=true` to enable full processor usage via `maxParallelForks`.
|
||||
- Use `fetch-depth: 0` only where needed (spotless ratcheting, version code). Use `fetch-depth: 1` otherwise.
|
||||
- Desktop build matrix: `macos-latest`, `windows-latest`, `ubuntu-24.04`, `ubuntu-24.04-arm`.
|
||||
- Lightweight jobs (labelers, triage, stale): use `ubuntu-24.04-arm` runners.
|
||||
- Gradle-heavy jobs: use `ubuntu-24.04` runners.
|
||||
20
.github/instructions/kmp-common.instructions.md
vendored
Normal file
20
.github/instructions/kmp-common.instructions.md
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
applyTo: "**/commonMain/**/*.kt"
|
||||
---
|
||||
|
||||
# KMP commonMain Rules
|
||||
|
||||
- NEVER import `java.*` or `android.*` in `commonMain`.
|
||||
- Use `org.meshtastic.core.common.util.ioDispatcher` instead of `Dispatchers.IO`.
|
||||
- Use Okio (`BufferedSource`/`BufferedSink`) instead of `java.io.*`.
|
||||
- Use `kotlinx.coroutines.sync.Mutex` instead of `java.util.concurrent.locks.*`.
|
||||
- Use `atomicfu` or Mutex-guarded `mutableMapOf()` instead of `ConcurrentHashMap`.
|
||||
- Use `jetbrains-*` catalog aliases for lifecycle/navigation dependencies.
|
||||
- Use `compose-multiplatform-*` catalog aliases for CMP dependencies.
|
||||
- Never use plain `androidx.compose` dependencies in `commonMain`.
|
||||
- Strings: use `stringResource(Res.string.key)` from `core:resources`. No hardcoded strings.
|
||||
- CMP `stringResource` only supports `%N$s` and `%N$d` — pre-format floats with `NumberFormatter.format()`.
|
||||
- Use `MetricFormatter` from `core:common` for display strings (temperature, voltage, percent, signal). Avoid scattered `formatString("%.1f°C", val)` calls.
|
||||
- Check `gradle/libs.versions.toml` before adding dependencies.
|
||||
- Use `safeCatching {}` from `core:common` instead of `runCatching {}` in coroutine/suspend contexts. Keep `runCatching` only in cleanup/teardown code.
|
||||
- Use `kotlinx.coroutines.CancellationException`, not `kotlin.coroutines.cancellation.CancellationException`.
|
||||
35
.github/labeler.yml
vendored
35
.github/labeler.yml
vendored
|
|
@ -1,35 +0,0 @@
|
|||
# Auto Labeler rulse using https://github.com/actions/labeler
|
||||
#
|
||||
|
||||
# 'fix' in title/branch -> bug
|
||||
# 'feat' in title/branch -> enhancement
|
||||
# 'repo' in title/branch OR changes to ~/.github/ -> repo
|
||||
# 'bug_fallthrough' for everything else except auto
|
||||
#
|
||||
# - [ ] need to look at title. waiting on https://github.com/actions/labeler/pull/866
|
||||
|
||||
# Add 'enhancement' label to any PR where the head branch name contains `feat`
|
||||
enhancement:
|
||||
- head-branch: [feat, Feat, FEAT]
|
||||
|
||||
# Add 'repo' label to any PR where the head branch name contains `repo`
|
||||
# or files in the .github dir
|
||||
repo:
|
||||
- any:
|
||||
- head-branch: [repo, Repo, REPO, ci, CI]
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: .github
|
||||
|
||||
# Add 'bug' label to any PR where the head branch name contains `fix` or `bug` as the prefix.
|
||||
bugfix:
|
||||
- head-branch: [^fix, ^bug, ^Fix, ^FIX, ^Bug, ^BUG]
|
||||
|
||||
# Add `refactor` label to any PR where the head branch name contains `refactor` or `Refactor` as the prefix.
|
||||
refactor:
|
||||
- head-branch: [^refactor, ^Refactor]
|
||||
|
||||
# our fallback - bug except repo, feat, or automated pipelines
|
||||
# bug_fallthrough:
|
||||
# - all:
|
||||
# - head-branch: ['^((?!feat).)*$', '^((?!repo).)*$', '^((?!renovate).)*$', '^((?!scheduled).)*$']
|
||||
|
||||
12
.github/lsp.json
vendored
Normal file
12
.github/lsp.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"lspServers": {
|
||||
"kotlin": {
|
||||
"command": "kotlin-language-server",
|
||||
"args": [],
|
||||
"fileExtensions": {
|
||||
".kt": "kotlin",
|
||||
".kts": "kotlin"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
223
.github/renovate.json
vendored
223
.github/renovate.json
vendored
|
|
@ -49,236 +49,31 @@
|
|||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Meshtastic Protobufs changelog link",
|
||||
"matchPackageNames": [
|
||||
"https://github.com/meshtastic/protobufs.git"
|
||||
],
|
||||
"changelogUrl": "https://github.com/meshtastic/protobufs/compare/{{currentDigest}}...{{newDigest}}",
|
||||
"groupName": "Meshtastic Protobufs",
|
||||
"groupSlug": "meshtastic-protobufs",
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Group all AndroidX dependencies (excluding more specific AndroidX groups)",
|
||||
"groupName": "AndroidX (General)",
|
||||
"groupSlug": "androidx-general",
|
||||
"description": "Group CMP and the androidx.compose artifacts that track it so Renovate bumps them together (see PR #5180)",
|
||||
"groupName": "compose-multiplatform",
|
||||
"matchPackageNames": [
|
||||
"/^androidx\\./",
|
||||
"!/^androidx\\.room/",
|
||||
"!/^androidx\\.lifecycle/",
|
||||
"!/^androidx\\.navigation/",
|
||||
"!/^androidx\\.datastore/",
|
||||
"!/^androidx\\.compose\\.material3\\.adaptive/",
|
||||
"!/^androidx\\.compose\\.material3:material3-adaptive-navigation-suite$/",
|
||||
"!/^androidx\\.test\\.espresso/",
|
||||
"!/^androidx\\.test\\.ext/",
|
||||
"!/^androidx\\.compose\\.ui:ui-test-junit4$/",
|
||||
"!/^androidx\\.hilt/"
|
||||
"/^org\\.jetbrains\\.compose/",
|
||||
"androidx.compose.runtime:runtime-tracing",
|
||||
"androidx.compose.ui:ui-test-manifest"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Group Kotlin standard library, coroutines, and serialization",
|
||||
"groupName": "Kotlin Ecosystem",
|
||||
"groupSlug": "kotlin",
|
||||
"matchPackageNames": [
|
||||
"/^org\\.jetbrains\\.kotlin/",
|
||||
"/^org\\.jetbrains\\.kotlinx/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Group Dagger and Hilt dependencies",
|
||||
"groupName": "Dagger & Hilt",
|
||||
"groupSlug": "hilt",
|
||||
"matchPackageNames": [
|
||||
"/^com\\.google\\.dagger/",
|
||||
"/^androidx\\.hilt/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Group Accompanist libraries",
|
||||
"groupName": "Accompanist",
|
||||
"groupSlug": "accompanist",
|
||||
"matchPackageNames": [
|
||||
"/^com\\.google\\.accompanist/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Group JVM testing libraries (JUnit, Mockito, Robolectric)",
|
||||
"groupName": "JVM Testing Libraries",
|
||||
"groupSlug": "jvm-testing",
|
||||
"matchPackageNames": [
|
||||
"/^junit:junit$/",
|
||||
"/^org\\.mockito:/",
|
||||
"/^org\\.robolectric:robolectric$/"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Group AndroidX Testing libraries",
|
||||
"groupName": "AndroidX Testing",
|
||||
"groupSlug": "androidx-testing",
|
||||
"matchPackageNames": [
|
||||
"/^androidx\\.test\\.espresso/",
|
||||
"/^androidx\\.test\\.ext/",
|
||||
"/^androidx\\.compose\\.ui:ui-test-junit4$/"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Group Static Analysis tools (Detekt, Spotless)",
|
||||
"groupName": "Static Analysis",
|
||||
"groupSlug": "static-analysis",
|
||||
"matchPackageNames": [
|
||||
"/^io\\.gitlab\\.arturbosch\\.detekt/",
|
||||
"/^io\\.nlopez\\.compose\\.rules/",
|
||||
"/^com\\.diffplug\\.spotless/"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Group Square networking libraries (OkHttp, Retrofit)",
|
||||
"groupName": "Square Networking",
|
||||
"groupSlug": "square-network",
|
||||
"matchPackageNames": [
|
||||
"/^com\\.squareup\\.okhttp3/",
|
||||
"/^com\\.squareup\\.retrofit2/"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Group Coil image loading library",
|
||||
"groupName": "Coil",
|
||||
"groupSlug": "coil",
|
||||
"matchPackageNames": [
|
||||
"/^io\\.coil-kt\\.coil3/"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Group ZXing barcode scanning libraries",
|
||||
"groupName": "ZXing",
|
||||
"groupSlug": "zxing",
|
||||
"matchPackageNames": [
|
||||
"/^com\\.journeyapps:zxing-android-embedded/",
|
||||
"/^com\\.google\\.zxing:core/"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Group Eclipse Paho MQTT client libraries",
|
||||
"groupName": "MQTT Paho Client",
|
||||
"groupSlug": "mqtt-paho",
|
||||
"matchPackageNames": [
|
||||
"/^org\\.eclipse\\.paho/"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Group Mike Penz Markdown renderer libraries",
|
||||
"groupName": "Markdown Renderer (Mike Penz)",
|
||||
"groupSlug": "markdown-renderer-mikepenz",
|
||||
"matchPackageNames": [
|
||||
"/^com\\.mikepenz/"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Group Firebase libraries",
|
||||
"groupName": "Firebase",
|
||||
"groupSlug": "firebase",
|
||||
"matchPackageNames": [
|
||||
"/^com\\.google\\.firebase/"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Group Datadog libraries",
|
||||
"groupName": "Datadog",
|
||||
"groupSlug": "datadog",
|
||||
"matchPackageNames": [
|
||||
"/^com\\.datadoghq/"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Group OpenStreetMap (OSM) libraries",
|
||||
"groupName": "OSM Libraries",
|
||||
"groupSlug": "osm-libraries",
|
||||
"matchPackageNames": [
|
||||
"/^org\\.osmdroid/",
|
||||
"/^com\\.github\\.MKergall\\.osmbonuspack/",
|
||||
"/^mil\\.nga/"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Group Google Maps Compose libraries",
|
||||
"groupName": "Google Maps Compose",
|
||||
"groupSlug": "google-maps-compose",
|
||||
"matchPackageNames": [
|
||||
"/^com\\.google\\.android\\.gms:play-services-location/",
|
||||
"/^com\\.google\\.maps\\.android/"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Group Google Protobuf runtime libraries",
|
||||
"groupName": "Protobuf Runtime",
|
||||
"groupSlug": "protobuf-runtime",
|
||||
"matchPackageNames": [
|
||||
"/^com\\.google\\.protobuf/",
|
||||
"!https://github.com/meshtastic/protobufs.git"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Group AndroidX Room libraries",
|
||||
"groupName": "AndroidX Room",
|
||||
"groupSlug": "androidx-room",
|
||||
"matchPackageNames": [
|
||||
"/^androidx\\.room/"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Group AndroidX Lifecycle libraries",
|
||||
"groupName": "AndroidX Lifecycle",
|
||||
"groupSlug": "androidx-lifecycle",
|
||||
"matchPackageNames": [
|
||||
"/^androidx\\.lifecycle/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Group AndroidX Navigation libraries",
|
||||
"groupName": "AndroidX Navigation",
|
||||
"groupSlug": "androidx-navigation",
|
||||
"matchPackageNames": [
|
||||
"/^androidx\\.navigation/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Group AndroidX DataStore libraries",
|
||||
"groupName": "AndroidX DataStore",
|
||||
"groupSlug": "androidx-datastore",
|
||||
"matchPackageNames": [
|
||||
"/^androidx\\.datastore/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Group AndroidX Adaptive UI libraries",
|
||||
"groupName": "AndroidX Adaptive UI",
|
||||
"groupSlug": "androidx-adaptive-ui",
|
||||
"matchPackageNames": [
|
||||
"/^androidx\\.compose\\.material3\\.adaptive/",
|
||||
"/^androidx\\.compose\\.material3:material3-adaptive-navigation-suite$/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Restrict sensitive infrastructure to patch updates only (manual minor)",
|
||||
"description": "Restrict sensitive infrastructure to manual minor updates",
|
||||
"matchUpdateTypes": [
|
||||
"minor"
|
||||
],
|
||||
"matchPackageNames": [
|
||||
"/^org\\.jetbrains\\.kotlin/",
|
||||
"/^org\\.jetbrains\\.kotlinx/",
|
||||
"/^org\\.jetbrains\\.compose/",
|
||||
"/^com\\.google\\.dagger/",
|
||||
"/^androidx\\.hilt/",
|
||||
"/^com\\.google\\.protobuf/",
|
||||
|
|
@ -298,4 +93,4 @@
|
|||
"automerge": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
107
.github/workflows/codeql.yml
vendored
107
.github/workflows/codeql.yml
vendored
|
|
@ -1,107 +0,0 @@
|
|||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL Advanced"
|
||||
|
||||
on:
|
||||
# push:
|
||||
# branches: [ "main" ]
|
||||
# pull_request:
|
||||
# branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# required to fetch internal or private CodeQL packs
|
||||
packages: read
|
||||
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
build-mode: none
|
||||
- language: java-kotlin
|
||||
build-mode: autobuild
|
||||
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
|
||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Add any setup steps before running the `github/codeql-action/init` action.
|
||||
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
||||
# or others). This is typically only required for manual builds.
|
||||
# - name: Setup runtime (example)
|
||||
# uses: actions/setup-example@v1
|
||||
- name: Java Setup
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# If the analyze step fails for one of the languages you are analyzing with
|
||||
# "We were unable to automatically build your code", modify the matrix above
|
||||
# to set the build mode to "manual" for that language. Then modify this step
|
||||
# to build your code.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
- if: matrix.build-mode == 'manual'
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||
'languages you are analyzing, replace this with the commands to build' \
|
||||
'your code, for example:'
|
||||
echo ' make bootstrap'
|
||||
echo ' make release'
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
134
.github/workflows/create-or-promote-release.yml
vendored
134
.github/workflows/create-or-promote-release.yml
vendored
|
|
@ -20,6 +20,11 @@ on:
|
|||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
build_desktop:
|
||||
description: 'Whether to build the desktop distribution'
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
|
@ -29,7 +34,7 @@ permissions:
|
|||
|
||||
jobs:
|
||||
determine-tags:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04-arm
|
||||
outputs:
|
||||
tag_to_process: ${{ steps.calculate_tags.outputs.tag_to_process }}
|
||||
release_name: ${{ steps.calculate_tags.outputs.release_name }}
|
||||
|
|
@ -106,112 +111,6 @@ jobs:
|
|||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Update External Assets (Firmware, Hardware, Protos)
|
||||
if: ${{ !inputs.dry_run && inputs.channel == 'internal' }}
|
||||
run: |
|
||||
# Update Submodules (Protobufs)
|
||||
echo "Updating core/proto submodule..."
|
||||
git submodule update --init --remote core/proto
|
||||
|
||||
# Update Firmware List
|
||||
firmware_file_path="app/src/main/assets/firmware_releases.json"
|
||||
temp_firmware_file="/tmp/new_firmware_releases.json"
|
||||
|
||||
echo "Fetching latest firmware releases..."
|
||||
curl -s --fail https://api.meshtastic.org/github/firmware/list > "$temp_firmware_file"
|
||||
|
||||
if ! jq empty "$temp_firmware_file" 2>/dev/null; then
|
||||
echo "::error::Firmware API returned invalid JSON data. Aborting."
|
||||
exit 1
|
||||
else
|
||||
if [ ! -f "$firmware_file_path" ] || ! jq --sort-keys . "$temp_firmware_file" | diff -q - <(jq --sort-keys . "$firmware_file_path"); then
|
||||
echo "Changes detected in firmware list or local file missing. Updating $firmware_file_path."
|
||||
cp "$temp_firmware_file" "$firmware_file_path"
|
||||
else
|
||||
echo "No changes detected in firmware list."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Update Hardware List
|
||||
hardware_file_path="app/src/main/assets/device_hardware.json"
|
||||
temp_hardware_file="/tmp/new_device_hardware.json"
|
||||
|
||||
echo "Fetching latest device hardware data..."
|
||||
curl -s --fail https://api.meshtastic.org/resource/deviceHardware > "$temp_hardware_file"
|
||||
|
||||
if ! jq empty "$temp_hardware_file" 2>/dev/null; then
|
||||
echo "::error::Hardware API returned invalid JSON data. Aborting."
|
||||
exit 1
|
||||
else
|
||||
if [ ! -f "$hardware_file_path" ] || ! jq --sort-keys . "$temp_hardware_file" | diff -q - <(jq --sort-keys . "$hardware_file_path"); then
|
||||
echo "Changes detected in hardware list or local file missing. Updating $hardware_file_path."
|
||||
cp "$temp_hardware_file" "$hardware_file_path"
|
||||
else
|
||||
echo "No changes detected in hardware list."
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Sync with Crowdin
|
||||
if: ${{ !inputs.dry_run && inputs.channel == 'internal' }}
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
base_url: 'https://meshtastic.crowdin.com/api/v2'
|
||||
config: 'crowdin.yml'
|
||||
crowdin_branch_name: 'main'
|
||||
upload_sources: true
|
||||
upload_sources_args: '--preserve-hierarchy'
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
download_translations_args: '--preserve-hierarchy'
|
||||
create_pull_request: false
|
||||
push_translations: false
|
||||
push_sources: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
- name: Commit Release Assets (Translations, Data, Config)
|
||||
if: ${{ !inputs.dry_run && inputs.channel == 'internal' }}
|
||||
env:
|
||||
FINAL_TAG: ${{ steps.calculate_tags.outputs.final_tag }}
|
||||
run: |
|
||||
# Calculate Version Code
|
||||
OFFSET=$(grep '^VERSION_CODE_OFFSET=' config.properties | cut -d'=' -f2)
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
# +1 because we are about to add a commit
|
||||
VERSION_CODE=$((COMMIT_COUNT + OFFSET + 1))
|
||||
|
||||
echo "Calculated Version Code: $VERSION_CODE"
|
||||
|
||||
# Update VERSION_NAME_BASE in config.properties
|
||||
sed -i "s/^VERSION_NAME_BASE=.*/VERSION_NAME_BASE=${{ inputs.base_version }}/" config.properties
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Add updated data files
|
||||
git add config.properties
|
||||
git add app/src/main/assets/firmware_releases.json || true
|
||||
git add app/src/main/assets/device_hardware.json || true
|
||||
git add core/proto || true
|
||||
|
||||
# Add updated translations (fastlane metadata and strings)
|
||||
git add fastlane/metadata/android || true
|
||||
git add "**/strings.xml" || true
|
||||
|
||||
# Only commit if there are changes
|
||||
if ! git diff --cached --quiet; then
|
||||
git commit -m "chore(release): prepare $FINAL_TAG [skip ci]
|
||||
|
||||
- Bump base version to ${{ inputs.base_version }}
|
||||
- Sync translations and assets"
|
||||
git push origin HEAD:${{ github.ref_name }}
|
||||
else
|
||||
echo "No changes to commit."
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Create and Push Release Tag
|
||||
if: ${{ !inputs.dry_run && inputs.channel == 'internal' }}
|
||||
env:
|
||||
|
|
@ -230,6 +129,7 @@ jobs:
|
|||
tag_name: ${{ needs.determine-tags.outputs.final_tag }}
|
||||
channel: ${{ inputs.channel }}
|
||||
base_version: ${{ inputs.base_version }}
|
||||
build_desktop: ${{ inputs.build_desktop }}
|
||||
secrets: inherit
|
||||
|
||||
call-promote-workflow:
|
||||
|
|
@ -244,3 +144,23 @@ jobs:
|
|||
base_version: ${{ inputs.base_version }}
|
||||
from_channel: ${{ needs.determine-tags.outputs.from_channel }}
|
||||
secrets: inherit
|
||||
|
||||
cleanup-on-failure:
|
||||
needs: [determine-tags, call-release-workflow]
|
||||
if: ${{ (failure() || cancelled()) && !inputs.dry_run && inputs.channel == 'internal' }}
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Delete Failed or Cancelled Tag
|
||||
env:
|
||||
FINAL_TAG: ${{ needs.determine-tags.outputs.final_tag }}
|
||||
run: |
|
||||
if [ -n "$FINAL_TAG" ]; then
|
||||
echo "Release workflow failed or was cancelled. Deleting tag $FINAL_TAG to allow a clean retry..."
|
||||
git push origin :refs/tags/"$FINAL_TAG" || echo "Tag was not pushed or already deleted."
|
||||
else
|
||||
echo "No tag was created to delete."
|
||||
fi
|
||||
|
|
|
|||
11
.github/workflows/dependency-submission.yml
vendored
11
.github/workflows/dependency-submission.yml
vendored
|
|
@ -10,19 +10,20 @@ permissions:
|
|||
|
||||
jobs:
|
||||
dependency-submission:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: jetbrains
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
token: ${{ github.token }}
|
||||
|
||||
- name: Generate and submit dependency graph
|
||||
uses: gradle/actions/dependency-submission@v5
|
||||
uses: gradle/actions/dependency-submission@v6
|
||||
with:
|
||||
build-scan-publish: true
|
||||
build-scan-terms-of-use-url: "https://gradle.com/help/legal-terms-of-use"
|
||||
build-scan-terms-of-use-url: "https://gradle.com/terms-of-service"
|
||||
build-scan-terms-of-use-agree: "yes"
|
||||
|
|
|
|||
34
.github/workflows/docs.yml
vendored
34
.github/workflows/docs.yml
vendored
|
|
@ -6,6 +6,16 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
# Only rebuild docs when source code changes (Dokka generates from KDoc)
|
||||
- 'app/src/**'
|
||||
- 'core/**/src/**'
|
||||
- 'feature/**/src/**'
|
||||
- 'desktop/src/**'
|
||||
- 'build-logic/**'
|
||||
- 'build.gradle.kts'
|
||||
- 'settings.gradle.kts'
|
||||
- '.github/workflows/docs.yml'
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
|
@ -29,16 +39,16 @@ permissions:
|
|||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
# Allow only one concurrent deployment; cancel queued runs since only the latest
|
||||
# main state matters for documentation.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-docs:
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
|
@ -47,20 +57,16 @@ jobs:
|
|||
submodules: 'recursive'
|
||||
ref: ${{ inputs.ref || '' }}
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'jetbrains'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
|
||||
- name: Build Dokka HTML documentation
|
||||
run: ./gradlew dokkaGeneratePublicationHtml
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: build/dokka/html
|
||||
|
||||
|
|
@ -69,9 +75,9 @@ jobs:
|
|||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04-arm
|
||||
needs: build-docs
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
uses: actions/deploy-pages@v5
|
||||
|
|
|
|||
26
.github/workflows/main-check.yml
vendored
Normal file
26
.github/workflows/main-check.yml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
name: Main CI (Verify & Build)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- 'docs/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: main-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
validate-and-build:
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
uses: ./.github/workflows/reusable-check.yml
|
||||
with:
|
||||
run_lint: true
|
||||
run_unit_tests: false
|
||||
run_desktop_builds: false
|
||||
upload_artifacts: true
|
||||
secrets: inherit
|
||||
10
.github/workflows/main-push-changelog.yml
vendored
10
.github/workflows/main-push-changelog.yml
vendored
|
|
@ -5,6 +5,10 @@ on:
|
|||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: main-push-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
|
@ -12,7 +16,7 @@ concurrency:
|
|||
jobs:
|
||||
main-push-changelog:
|
||||
name: Generate main push changelog
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
|
@ -35,6 +39,10 @@ jobs:
|
|||
fromTag: ${{ steps.last_prod_tag.outputs.tag }}
|
||||
toTag: ${{ github.sha }}
|
||||
outputFile: main-push-changelog.md
|
||||
fetchViaCommits: true
|
||||
fetchReviewers: false
|
||||
fetchReleaseInformation: false
|
||||
fetchReviews: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
|
|
|||
10
.github/workflows/merge-queue.yml
vendored
10
.github/workflows/merge-queue.yml
vendored
|
|
@ -4,6 +4,9 @@ on:
|
|||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: build-mq-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
|
@ -13,14 +16,15 @@ jobs:
|
|||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
uses: ./.github/workflows/reusable-check.yml
|
||||
with:
|
||||
api_levels: '[26, 35]' # Comprehensive testing for Merge Queue
|
||||
flavors: '["google", "fdroid"]'
|
||||
run_lint: true
|
||||
run_unit_tests: true
|
||||
upload_artifacts: false
|
||||
secrets: inherit
|
||||
|
||||
check-workflow-status:
|
||||
name: Check Workflow Status
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04-arm
|
||||
permissions: {}
|
||||
needs:
|
||||
- android-check
|
||||
if: always()
|
||||
|
|
|
|||
36
.github/workflows/models_issue_triage.yml
vendored
36
.github/workflows/models_issue_triage.yml
vendored
|
|
@ -14,8 +14,8 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
triage:
|
||||
if: ${{ github.repository == 'meshtastic/firmware' && github.event.issue.user.type != 'Bot' }}
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'meshtastic/Meshtastic-Android' && github.event.issue.user.type != 'Bot' }}
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Step 1: Quality check (spam/AI-slop detection) - runs first, exits early if spam
|
||||
|
|
@ -38,7 +38,7 @@ jobs:
|
|||
|
||||
- name: Apply quality label if needed
|
||||
if: steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok'
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
QUALITY_LABEL: ${{ steps.quality.outputs.response }}
|
||||
with:
|
||||
|
|
@ -80,7 +80,7 @@ jobs:
|
|||
# ─────────────────────────────────────────────────────────────────────────
|
||||
- name: Determine if completeness check should be skipped
|
||||
if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == ''
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
id: check-skip
|
||||
with:
|
||||
script: |
|
||||
|
|
@ -98,20 +98,20 @@ jobs:
|
|||
continue-on-error: true
|
||||
with:
|
||||
prompt: |
|
||||
Analyze this GitHub issue for completeness and determine if it needs labels.
|
||||
Analyze this GitHub issue for the Meshtastic Android app and determine if it needs labels.
|
||||
|
||||
If this looks like a bug on the device/firmware (crash, reboot, lockup, radio issues, GPS issues, display issues, power/sleep issues), request device logs and explain how to get them:
|
||||
If this looks like a bug in the Android app (crash, ANR, UI glitch, connection failure, Bluetooth issues, notification problems, map issues), request app logs and explain how to get them:
|
||||
|
||||
Web Flasher logs:
|
||||
- Go to https://flasher.meshtastic.org
|
||||
- Connect the device via USB and click Connect
|
||||
- Open the device console/log output, reproduce the problem, then copy/download and attach/paste the logs
|
||||
Android app debug logs:
|
||||
- Open the Meshtastic app, go to Settings > Debug > Save Logs
|
||||
- Reproduce the problem, then share/attach the exported log file
|
||||
|
||||
Meshtastic CLI logs:
|
||||
- Run: meshtastic --port <serial-port> --noproto
|
||||
- Reproduce the problem, then copy/paste the terminal output
|
||||
Android logcat (if app logs are insufficient):
|
||||
- Connect phone via USB with USB debugging enabled
|
||||
- Run: adb logcat -s Meshtastic:* *:E
|
||||
- Reproduce the problem, then copy/paste the relevant output
|
||||
|
||||
Also request key context if missing: device model/variant, firmware version, region, steps to reproduce, expected vs actual.
|
||||
Also request key context if missing: Android version, phone model, app version, Meshtastic device model, firmware version, connection type (BLE/USB/TCP), steps to reproduce, expected vs actual.
|
||||
|
||||
Respond ONLY with JSON:
|
||||
{
|
||||
|
|
@ -120,7 +120,7 @@ jobs:
|
|||
"label": "needs-logs" | "needs-info" | "none"
|
||||
}
|
||||
|
||||
Use "needs-logs" if this is a device bug AND no logs are attached.
|
||||
Use "needs-logs" if this is an app bug AND no logs are attached.
|
||||
Use "needs-info" if basic info like firmware version or steps to reproduce are missing.
|
||||
Use "none" if the issue is complete or is a feature request.
|
||||
|
||||
|
|
@ -131,7 +131,7 @@ jobs:
|
|||
|
||||
- name: Process analysis result
|
||||
if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' && steps.analysis.outputs.response != ''
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
id: process
|
||||
env:
|
||||
AI_RESPONSE: ${{ steps.analysis.outputs.response }}
|
||||
|
|
@ -165,7 +165,7 @@ jobs:
|
|||
|
||||
- name: Apply triage label
|
||||
if: steps.process.outputs.label != '' && steps.process.outputs.label != 'none'
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
LABEL_NAME: ${{ steps.process.outputs.label }}
|
||||
with:
|
||||
|
|
@ -191,7 +191,7 @@ jobs:
|
|||
|
||||
- name: Comment on issue
|
||||
if: steps.process.outputs.should_comment == 'true'
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
COMMENT_BODY: ${{ steps.process.outputs.comment_body }}
|
||||
with:
|
||||
|
|
|
|||
40
.github/workflows/models_pr_triage.yml
vendored
40
.github/workflows/models_pr_triage.yml
vendored
|
|
@ -15,19 +15,19 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
triage:
|
||||
if: ${{ github.repository == 'meshtastic/firmware' && github.event.pull_request.user.type != 'Bot' }}
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'meshtastic/Meshtastic-Android' && github.event.pull_request.user.type != 'Bot' }}
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Step 1: Check if PR already has automation/type labels (skip if so)
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
- name: Check existing labels
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
id: check-labels
|
||||
with:
|
||||
script: |
|
||||
const skipLabels = new Set(['automation']);
|
||||
const typeLabels = new Set(['bugfix', 'hardware-support', 'enhancement', 'dependencies', 'submodules', 'github_actions', 'trunk', 'cleanup']);
|
||||
const skipLabels = new Set(['automation', 'release']);
|
||||
const typeLabels = new Set(['bugfix', 'enhancement', 'dependencies', 'repo', 'refactor']);
|
||||
const prLabels = context.payload.pull_request.labels.map(l => l.name);
|
||||
|
||||
const shouldSkipAll = prLabels.some(l => skipLabels.has(l));
|
||||
|
|
@ -44,13 +44,16 @@ jobs:
|
|||
uses: actions/ai-inference@v2
|
||||
id: quality
|
||||
continue-on-error: true
|
||||
env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
with:
|
||||
max-tokens: 20
|
||||
prompt: |
|
||||
Is this GitHub pull request spam, AI-generated slop, or low quality?
|
||||
|
||||
Title: ${{ github.event.pull_request.title }}
|
||||
Body: ${{ github.event.pull_request.body }}
|
||||
Title: ${{ env.PR_TITLE }}
|
||||
Body: ${{ env.PR_BODY }}
|
||||
|
||||
Respond with exactly one of: spam, ai-generated, needs-review, ok
|
||||
system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop.
|
||||
|
|
@ -58,7 +61,7 @@ jobs:
|
|||
|
||||
- name: Apply quality label if needed
|
||||
if: steps.check-labels.outputs.skip_all != 'true' && steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok'
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
id: quality-label
|
||||
env:
|
||||
QUALITY_LABEL: ${{ steps.quality.outputs.response }}
|
||||
|
|
@ -87,32 +90,35 @@ jobs:
|
|||
core.setOutput('is_spam', 'true');
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Step 3: Auto-label PR type (bugfix/hardware-support/enhancement)
|
||||
# Step 3: Auto-label PR type (bugfix/enhancement/refactor)
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
- name: Classify PR for labeling
|
||||
if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '')
|
||||
uses: actions/ai-inference@v2
|
||||
id: classify
|
||||
continue-on-error: true
|
||||
env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
with:
|
||||
max-tokens: 30
|
||||
prompt: |
|
||||
Classify this pull request into exactly one category.
|
||||
Classify this pull request for the Meshtastic Android app into exactly one category.
|
||||
|
||||
Return exactly one of: bugfix, hardware-support, enhancement
|
||||
Return exactly one of: bugfix, enhancement, refactor
|
||||
|
||||
Use bugfix if it fixes a bug, crash, or incorrect behavior.
|
||||
Use hardware-support if it adds or improves support for a specific hardware device/variant.
|
||||
Use enhancement if it adds a new feature, improves performance, or refactors code.
|
||||
Use enhancement if it adds a new feature, improves performance, or adds new functionality.
|
||||
Use refactor if it restructures code without changing behavior, cleans up code, or improves architecture.
|
||||
|
||||
Title: ${{ github.event.pull_request.title }}
|
||||
Body: ${{ github.event.pull_request.body }}
|
||||
Title: ${{ env.PR_TITLE }}
|
||||
Body: ${{ env.PR_BODY }}
|
||||
system-prompt: You classify pull requests into categories. Be conservative and pick the most appropriate single label.
|
||||
model: openai/gpt-4o-mini
|
||||
|
||||
- name: Apply type label
|
||||
if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.classify.outputs.response != ''
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
TYPE_LABEL: ${{ steps.classify.outputs.response }}
|
||||
with:
|
||||
|
|
@ -120,8 +126,8 @@ jobs:
|
|||
const label = (process.env.TYPE_LABEL || '').trim().toLowerCase();
|
||||
const labelMeta = {
|
||||
'bugfix': { color: 'd73a4a', description: 'Bug fix' },
|
||||
'hardware-support': { color: '0e8a16', description: 'Hardware support addition or improvement' },
|
||||
'enhancement': { color: 'a2eeef', description: 'New feature or enhancement' },
|
||||
'refactor': { color: 'c5def5', description: 'Code restructuring without behavior change' },
|
||||
};
|
||||
const meta = labelMeta[label];
|
||||
if (!meta) return;
|
||||
|
|
|
|||
3
.github/workflows/moderate.yml
vendored
3
.github/workflows/moderate.yml
vendored
|
|
@ -9,7 +9,8 @@ on:
|
|||
|
||||
jobs:
|
||||
spam-detection:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
runs-on: ubuntu-24.04-arm
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
|
|
|||
2
.github/workflows/post-release-cleanup.yml
vendored
2
.github/workflows/post-release-cleanup.yml
vendored
|
|
@ -18,7 +18,7 @@ permissions:
|
|||
|
||||
jobs:
|
||||
cleanup_prereleases:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04-arm
|
||||
environment: Release
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
|
|
|||
29
.github/workflows/pr_enforce_labels.yml
vendored
29
.github/workflows/pr_enforce_labels.yml
vendored
|
|
@ -4,29 +4,34 @@ on:
|
|||
pull_request:
|
||||
types: [edited, labeled]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-label:
|
||||
runs-on: ubuntu-latest
|
||||
check-label:
|
||||
# Skip bot PRs — they already have labels from the workflows/bots that create them
|
||||
if: >-
|
||||
github.event.pull_request.user.login != 'renovate[bot]' &&
|
||||
github.event.pull_request.user.login != 'github-actions[bot]' &&
|
||||
github.event.pull_request.user.login != 'dependabot[bot]' &&
|
||||
github.event.pull_request.head.ref != 'scheduled-updates' &&
|
||||
github.event.pull_request.head.ref != 'l10n_main'
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Check for PR labels
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
// Always fetch the latest labels from the GitHub API to avoid stale context
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber,
|
||||
});
|
||||
const latestLabels = pr.labels.map(label => label.name);
|
||||
// Extract labels from the payload directly to avoid extra API calls
|
||||
const latestLabels = context.payload.pull_request.labels.map(label => label.name);
|
||||
const requiredLabels = ['bugfix', 'enhancement', 'automation', 'dependencies', 'repo', 'release', 'refactor'];
|
||||
console.log('Labels from payload:', latestLabels);
|
||||
const hasRequiredLabel = latestLabels.some(label => requiredLabels.includes(label));
|
||||
console.log('Latest labels:', latestLabels);
|
||||
if (!hasRequiredLabel) {
|
||||
core.setFailed(`PR must have at least one of the following labels before it can be merged: ${requiredLabels.join(', ')}.`);
|
||||
}
|
||||
|
|
|
|||
20
.github/workflows/promote.yml
vendored
20
.github/workflows/promote.yml
vendored
|
|
@ -65,9 +65,9 @@ permissions:
|
|||
|
||||
jobs:
|
||||
prepare-build-info:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04-arm
|
||||
outputs:
|
||||
APP_VERSION_NAME: ${{ steps.get_version_name.outputs.APP_VERSION_NAME }}
|
||||
APP_VERSION_NAME: ${{ steps.prep_version.outputs.APP_VERSION_NAME }}
|
||||
APP_VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
|
@ -77,9 +77,14 @@ jobs:
|
|||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Determine Version Name from Tag
|
||||
id: get_version_name
|
||||
run: echo "APP_VERSION_NAME=$(echo ${{ inputs.tag_name }} | sed 's/-.*//' | sed 's/v//')" >> $GITHUB_OUTPUT
|
||||
- name: Prep APP_VERSION_NAME
|
||||
id: prep_version
|
||||
env:
|
||||
INPUT_TAG_NAME: ${{ inputs.tag_name }}
|
||||
run: |
|
||||
VERSION_NAME=$(echo $INPUT_TAG_NAME | sed 's/-.*//' | sed 's/v//')
|
||||
echo "APP_VERSION_NAME=$VERSION_NAME" >> $GITHUB_OUTPUT
|
||||
echo "Parsed Version: $VERSION_NAME"
|
||||
|
||||
- name: Extract VERSION_CODE_OFFSET from config.properties
|
||||
id: get_version_code_offset
|
||||
|
|
@ -97,7 +102,7 @@ jobs:
|
|||
shell: bash
|
||||
|
||||
promote-release:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04-arm
|
||||
environment: Release
|
||||
needs: [ prepare-build-info ]
|
||||
steps:
|
||||
|
|
@ -111,7 +116,7 @@ jobs:
|
|||
user-fraction: ${{ (inputs.channel == 'production' && '0.1') || (inputs.channel == 'open' && '0.5') || '1.0' }}
|
||||
|
||||
update-github-release:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04-arm
|
||||
needs: [ prepare-build-info, promote-release ]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
|
@ -134,6 +139,7 @@ jobs:
|
|||
gh release edit ${{ inputs.tag_name }} \
|
||||
--tag ${{ inputs.final_tag }} \
|
||||
--title "${{ inputs.release_name }} (${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }})" \
|
||||
--draft=false \
|
||||
--prerelease=${{ inputs.channel != 'production' }}
|
||||
|
||||
- name: Notify Discord
|
||||
|
|
|
|||
22
.github/workflows/publish-core.yml
vendored
22
.github/workflows/publish-core.yml
vendored
|
|
@ -12,7 +12,7 @@ on:
|
|||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
|
@ -23,25 +23,25 @@ jobs:
|
|||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
|
||||
- name: Configure Version
|
||||
id: version
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
VERSION_SUFFIX: ${{ inputs.version_suffix }}
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "release" ]]; then
|
||||
echo "VERSION_NAME=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
|
||||
if [[ "$EVENT_NAME" == "release" ]]; then
|
||||
echo "VERSION_NAME=$RELEASE_TAG" >> $GITHUB_ENV
|
||||
else
|
||||
# Use a timestamp-based version for manual/branch builds to avoid collisions
|
||||
# or use the base version + suffix
|
||||
BASE_VERSION=$(grep "VERSION_NAME_BASE" config.properties | cut -d'=' -f2)
|
||||
echo "VERSION_NAME=${BASE_VERSION}${{ inputs.version_suffix }}" >> $GITHUB_ENV
|
||||
echo "VERSION_NAME=${BASE_VERSION}${VERSION_SUFFIX}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Publish to GitHub Packages
|
||||
|
|
|
|||
62
.github/workflows/pull-request-target.yml
vendored
62
.github/workflows/pull-request-target.yml
vendored
|
|
@ -1,15 +1,67 @@
|
|||
name: "Pull Request Labeler"
|
||||
on:
|
||||
- pull_request_target
|
||||
# Do not execute arbitary code on this workflow.
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
# Do not execute arbitrary code on this workflow.
|
||||
# See warnings at https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- id: label-the-PR
|
||||
uses: actions/labeler@v6
|
||||
- name: Auto-label PR
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const branch = context.payload.pull_request.head.ref;
|
||||
const labels = new Set();
|
||||
|
||||
// enhancement: branch contains feat
|
||||
if (/feat/i.test(branch)) labels.add('enhancement');
|
||||
|
||||
// bugfix: branch starts with fix or bug
|
||||
if (/^(fix|bug)/i.test(branch)) labels.add('bugfix');
|
||||
|
||||
// refactor: branch starts with refactor
|
||||
if (/^refactor/i.test(branch)) labels.add('refactor');
|
||||
|
||||
// repo: branch contains repo or ci
|
||||
if (/repo|ci/i.test(branch)) {
|
||||
labels.add('repo');
|
||||
} else {
|
||||
// Also label 'repo' if .github files were changed (needs one API call)
|
||||
try {
|
||||
const files = await github.paginate(
|
||||
github.rest.pulls.listFiles,
|
||||
{ owner: context.repo.owner, repo: context.repo.repo, pull_number: context.payload.pull_request.number, per_page: 100 },
|
||||
(res) => res.data.map(f => f.filename)
|
||||
);
|
||||
if (files.some(f => f.startsWith('.github/'))) labels.add('repo');
|
||||
} catch (e) {
|
||||
core.warning(`Could not list PR files (rate limited?): ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (labels.size > 0) {
|
||||
const labelArray = [...labels];
|
||||
core.info(`Applying labels: ${labelArray.join(', ')}`);
|
||||
try {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
labels: labelArray,
|
||||
});
|
||||
} catch (e) {
|
||||
core.warning(`Could not apply labels (rate limited?): ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
core.info('No labels matched for this PR.');
|
||||
}
|
||||
|
|
|
|||
140
.github/workflows/pull-request.yml
vendored
140
.github/workflows/pull-request.yml
vendored
|
|
@ -1,72 +1,134 @@
|
|||
name: Pull Request CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
branches: [ main ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: build-pr-${{ github.ref }}
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# 1. CHANGE DETECTION: Prevents unnecessary builds
|
||||
check-changes:
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android' && !( github.head_ref == 'scheduled-updates' || github.head_ref == 'l10n_main' )
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04-arm
|
||||
outputs:
|
||||
code_changed: ${{ steps.filter.outputs.code }}
|
||||
android: ${{ steps.filter.outputs.android }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dorny/paths-filter@v3
|
||||
- uses: dorny/paths-filter@v4
|
||||
id: filter
|
||||
with:
|
||||
token: ''
|
||||
filters: |
|
||||
code:
|
||||
- '**/*.kt'
|
||||
- '**/*.java'
|
||||
- '**/*.xml'
|
||||
- '**/*.kts'
|
||||
- '**/*.properties'
|
||||
android:
|
||||
# CI/workflow implementation
|
||||
- '.github/workflows/**'
|
||||
- '.github/actions/**'
|
||||
# Product modules validated by reusable-check
|
||||
- 'app/**'
|
||||
- 'baselineprofile/**'
|
||||
- 'desktop/**'
|
||||
- 'core/**'
|
||||
- 'feature/**'
|
||||
# Shared build infrastructure
|
||||
- 'build-logic/**'
|
||||
- 'config/**'
|
||||
- 'gradle/**'
|
||||
# Root build entrypoints/config that can alter task graph or outputs
|
||||
- 'build.gradle.kts'
|
||||
- 'config.properties'
|
||||
- 'compose_compiler_config.conf'
|
||||
- 'gradle.properties'
|
||||
- 'gradlew'
|
||||
- 'gradlew.bat'
|
||||
- '**/src/**'
|
||||
- '.github/workflows/**'
|
||||
- 'settings.gradle.kts'
|
||||
- 'test.gradle.kts'
|
||||
|
||||
android-check:
|
||||
# 1b. FILTER DRIFT CHECK: Ensures check-changes stays aligned with module roots
|
||||
verify-check-changes-filter:
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android' && !( github.head_ref == 'scheduled-updates' || github.head_ref == 'l10n_main' )
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Verify module roots are represented in check-changes filter
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
settings = Path('settings.gradle.kts').read_text()
|
||||
workflow = Path('.github/workflows/pull-request.yml').read_text()
|
||||
|
||||
module_roots = {
|
||||
module.split(':')[0]
|
||||
for module in re.findall(r'":([^"]+)"', settings)
|
||||
}
|
||||
|
||||
allowed_extra_roots = {'baselineprofile'}
|
||||
expected_roots = module_roots | allowed_extra_roots
|
||||
|
||||
filter_paths = {
|
||||
path.split('/')[0]
|
||||
for path in re.findall(r"-\s*'([^']+/\*\*)'", workflow)
|
||||
}
|
||||
|
||||
actual_module_roots = filter_paths & expected_roots
|
||||
|
||||
missing = sorted(expected_roots - actual_module_roots)
|
||||
unexpected = sorted(actual_module_roots - expected_roots)
|
||||
|
||||
if missing or unexpected:
|
||||
print('check-changes filter drift detected:')
|
||||
if missing:
|
||||
print(' Missing roots:', ', '.join(missing))
|
||||
if unexpected:
|
||||
print(' Unexpected roots:', ', '.join(unexpected))
|
||||
raise SystemExit(1)
|
||||
|
||||
print('check-changes filter is aligned with settings.gradle module roots.')
|
||||
PY
|
||||
|
||||
# 2. VALIDATION & BUILD: Delegate to reusable-check.yml
|
||||
# We disable coverage and desktop builds for PRs to keep feedback fast
|
||||
# (< 10 mins). Desktop compilation is already covered by the :desktop:test
|
||||
# task in the shard-app test shard.
|
||||
validate-and-build:
|
||||
needs: check-changes
|
||||
if: needs.check-changes.outputs.code_changed == 'true'
|
||||
if: needs.check-changes.outputs.android == 'true'
|
||||
uses: ./.github/workflows/reusable-check.yml
|
||||
with:
|
||||
api_levels: '[35]' # Only test latest API on PRs for speed
|
||||
flavors: '["google","fdroid"]'
|
||||
run_lint: true
|
||||
run_unit_tests: true
|
||||
run_coverage: false
|
||||
run_desktop_builds: false
|
||||
upload_artifacts: true
|
||||
secrets: inherit
|
||||
|
||||
skip-notice:
|
||||
needs: check-changes
|
||||
if: needs.check-changes.outputs.code_changed != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Skip CI for non-code changes
|
||||
run: echo "Skipping CI - no code changes detected (docs/config only)"
|
||||
|
||||
# 3. WORKFLOW STATUS: Ensures required checks are satisfied
|
||||
check-workflow-status:
|
||||
name: Check Workflow Status
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check-changes
|
||||
- android-check
|
||||
runs-on: ubuntu-24.04-arm
|
||||
permissions: {}
|
||||
needs: [check-changes, verify-check-changes-filter, validate-and-build]
|
||||
if: always()
|
||||
steps:
|
||||
- name: Check Workflow Status
|
||||
run: |
|
||||
if [[ "${{ needs.check-changes.outputs.code_changed }}" != "true" ]]; then
|
||||
echo "No code changes - CI jobs skipped as expected"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${{ needs.android-check.result }}" == "failure" || "${{ needs.android-check.result }}" == "cancelled" ]]; then
|
||||
echo "::error::Android Check failed"
|
||||
if [[ "${{ needs.verify-check-changes-filter.result }}" == "failure" || "${{ needs.verify-check-changes-filter.result }}" == "cancelled" ]]; then
|
||||
echo "::error::check-changes filter verification failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All jobs passed successfully"
|
||||
# If changes were detected but build failed, fail the status check
|
||||
if [[ "${{ needs.check-changes.outputs.android }}" == "true" && ("${{ needs.validate-and-build.result }}" == "failure" || "${{ needs.validate-and-build.result }}" == "cancelled") ]]; then
|
||||
echo "::error::Android Check failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If no changes were detected, this still succeeds to satisfy required status check
|
||||
echo "Workflow status satisfied."
|
||||
|
|
|
|||
191
.github/workflows/release.yml
vendored
191
.github/workflows/release.yml
vendored
|
|
@ -19,6 +19,11 @@ on:
|
|||
description: 'The channel to create a release for or promote to'
|
||||
required: true
|
||||
type: string
|
||||
build_desktop:
|
||||
description: 'Whether to build the desktop distribution'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
secrets:
|
||||
GSERVICES:
|
||||
required: true
|
||||
|
|
@ -44,6 +49,10 @@ on:
|
|||
required: false
|
||||
GRADLE_CACHE_PASSWORD:
|
||||
required: false
|
||||
INTERNAL_BUILDS_HOST:
|
||||
required: false
|
||||
INTERNAL_BUILDS_HOST_PAT:
|
||||
required: false
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ inputs.tag_name }}
|
||||
|
|
@ -56,23 +65,12 @@ permissions:
|
|||
attestations: write
|
||||
|
||||
jobs:
|
||||
run-lint:
|
||||
uses: ./.github/workflows/reusable-check.yml
|
||||
with:
|
||||
run_lint: true
|
||||
run_unit_tests: false
|
||||
run_instrumented_tests: false
|
||||
flavors: '["google"]'
|
||||
upload_artifacts: false
|
||||
secrets: inherit
|
||||
|
||||
prepare-build-info:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04-arm
|
||||
outputs:
|
||||
APP_VERSION_NAME: ${{ steps.get_version_name.outputs.APP_VERSION_NAME }}
|
||||
APP_VERSION_NAME: ${{ steps.prep_version.outputs.APP_VERSION_NAME }}
|
||||
APP_VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }}
|
||||
env:
|
||||
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
|
||||
GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }}
|
||||
GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }}
|
||||
GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }}
|
||||
|
|
@ -83,22 +81,14 @@ jobs:
|
|||
ref: ${{ inputs.tag_name }}
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'jetbrains'
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
build-scan-publish: true
|
||||
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
|
||||
build-scan-terms-of-use-agree: 'yes'
|
||||
|
||||
- name: Determine Version Name from Tag
|
||||
id: get_version_name
|
||||
run: echo "APP_VERSION_NAME=$(echo ${{ inputs.tag_name }} | sed 's/-.*//' | sed 's/v//')" >> $GITHUB_OUTPUT
|
||||
- name: Prep APP_VERSION_NAME
|
||||
id: prep_version
|
||||
env:
|
||||
INPUT_TAG_NAME: ${{ inputs.tag_name }}
|
||||
run: |
|
||||
VERSION_NAME=$(echo $INPUT_TAG_NAME | sed 's/-.*//' | sed 's/v//')
|
||||
echo "APP_VERSION_NAME=$VERSION_NAME" >> $GITHUB_OUTPUT
|
||||
echo "Parsed Version: $VERSION_NAME"
|
||||
|
||||
- name: Extract VERSION_CODE_OFFSET from config.properties
|
||||
id: get_version_code_offset
|
||||
|
|
@ -116,11 +106,10 @@ jobs:
|
|||
shell: bash
|
||||
|
||||
release-google:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare-build-info, run-lint]
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [prepare-build-info]
|
||||
environment: Release
|
||||
env:
|
||||
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
|
||||
GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }}
|
||||
GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }}
|
||||
GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }}
|
||||
|
|
@ -131,18 +120,12 @@ jobs:
|
|||
ref: ${{ inputs.tag_name }}
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'jetbrains'
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
build-scan-publish: true
|
||||
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
|
||||
build-scan-terms-of-use-agree: 'yes'
|
||||
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
cache_read_only: 'false'
|
||||
|
||||
- name: Load secrets
|
||||
env:
|
||||
|
|
@ -167,7 +150,7 @@ jobs:
|
|||
- name: Setup Fastlane
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.4.8'
|
||||
ruby-version: '3.4.9'
|
||||
bundler-cache: true
|
||||
|
||||
- name: Build and Deploy Google Play to Internal Track with Fastlane
|
||||
|
|
@ -192,27 +175,26 @@ jobs:
|
|||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: google-apk
|
||||
path: app/build/outputs/apk/**/*.apk
|
||||
path: app/build/outputs/apk/google/release/*.apk
|
||||
retention-days: 1
|
||||
|
||||
- name: Attest Google AAB provenance
|
||||
if: always()
|
||||
if: success()
|
||||
uses: actions/attest-build-provenance@v4
|
||||
with:
|
||||
subject-path: app/build/outputs/bundle/googleRelease/app-google-release.aab
|
||||
|
||||
- name: Attest Google APK provenance
|
||||
if: always()
|
||||
if: success()
|
||||
uses: actions/attest-build-provenance@v4
|
||||
with:
|
||||
subject-path: app/build/outputs/apk/**/*.apk
|
||||
subject-path: app/build/outputs/apk/google/release/*.apk
|
||||
|
||||
release-fdroid:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare-build-info, run-lint]
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [prepare-build-info]
|
||||
environment: Release
|
||||
env:
|
||||
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
|
||||
GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }}
|
||||
GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }}
|
||||
GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }}
|
||||
|
|
@ -223,18 +205,12 @@ jobs:
|
|||
ref: ${{ inputs.tag_name }}
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'jetbrains'
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
build-scan-publish: true
|
||||
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
|
||||
build-scan-terms-of-use-agree: 'yes'
|
||||
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
cache_read_only: 'false'
|
||||
|
||||
- name: Load secrets
|
||||
env:
|
||||
|
|
@ -248,7 +224,7 @@ jobs:
|
|||
- name: Setup Fastlane
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.4.8'
|
||||
ruby-version: '3.4.9'
|
||||
bundler-cache: true
|
||||
|
||||
- name: Build F-Droid with Fastlane
|
||||
|
|
@ -265,24 +241,86 @@ jobs:
|
|||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: fdroid-apk
|
||||
path: app/build/outputs/apk/**/*.apk
|
||||
path: app/build/outputs/apk/fdroid/release/*.apk
|
||||
retention-days: 1
|
||||
|
||||
- name: Attest F-Droid APK provenance
|
||||
if: always()
|
||||
if: success()
|
||||
uses: actions/attest-build-provenance@v4
|
||||
with:
|
||||
subject-path: app/build/outputs/apk/**/*.apk
|
||||
subject-path: app/build/outputs/apk/fdroid/release/*.apk
|
||||
|
||||
github-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare-build-info, release-google, release-fdroid]
|
||||
release-desktop:
|
||||
if: ${{ inputs.build_desktop }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: [prepare-build-info]
|
||||
environment: Release
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-latest, windows-latest, ubuntu-24.04, ubuntu-24.04-arm]
|
||||
env:
|
||||
GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }}
|
||||
GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }}
|
||||
GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.tag_name }}
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
with:
|
||||
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
cache_read_only: 'false'
|
||||
|
||||
- name: Install dependencies for AppImage
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get update && sudo apt-get install -y libfuse2
|
||||
|
||||
- name: Package Native Distributions
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_appVersionName: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
|
||||
APPIMAGE_EXTRACT_AND_RUN: 1
|
||||
run: ./gradlew :desktop:packageReleaseDistributionForCurrentOS -PaboutLibraries.release=true --no-daemon
|
||||
|
||||
- name: List Desktop Binaries
|
||||
if: runner.os == 'Linux'
|
||||
run: ls -R desktop/build/compose/binaries/main-release
|
||||
|
||||
- name: Upload Desktop Artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: desktop-${{ runner.os }}-${{ runner.arch }}
|
||||
path: |
|
||||
desktop/build/compose/binaries/main-release/*/*.dmg
|
||||
desktop/build/compose/binaries/main-release/*/*.msi
|
||||
desktop/build/compose/binaries/main-release/*/*.exe
|
||||
desktop/build/compose/binaries/main-release/*/*.deb
|
||||
desktop/build/compose/binaries/main-release/*/*.rpm
|
||||
desktop/build/compose/binaries/main-release/*/*.AppImage
|
||||
retention-days: 1
|
||||
if-no-files-found: ignore
|
||||
|
||||
github-release:
|
||||
if: ${{ !cancelled() && !failure() }}
|
||||
runs-on: ubuntu-24.04-arm
|
||||
needs: [prepare-build-info, release-google, release-fdroid, release-desktop]
|
||||
env:
|
||||
INTERNAL_BUILDS_HOST: ${{ secrets.INTERNAL_BUILDS_HOST }}
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.tag_name }}
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
|
|
@ -290,23 +328,26 @@ jobs:
|
|||
path: ./artifacts
|
||||
|
||||
- name: Create or Update GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
tag_name: ${{ inputs.tag_name }}
|
||||
target_commitish: ${{ inputs.commit_sha || github.sha }}
|
||||
name: ${{ inputs.tag_name }} (${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }})
|
||||
generate_release_notes: true
|
||||
files: ./artifacts/*/*
|
||||
files: ./artifacts/**/*
|
||||
draft: true
|
||||
prerelease: true
|
||||
|
||||
- name: Create or Update internal GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
continue-on-error: true
|
||||
if: ${{ env.INTERNAL_BUILDS_HOST != '' }}
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
repository: ${{ secrets.INTERNAL_BUILDS_HOST }}
|
||||
token: ${{ secrets.INTERNAL_BUILDS_HOST_PAT }}
|
||||
tag_name: ${{ inputs.tag_name }}
|
||||
name: ${{ inputs.tag_name }} (${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }})
|
||||
generate_release_notes: true
|
||||
files: ./artifacts/*/*
|
||||
generate_release_notes: false
|
||||
files: ./artifacts/**/*
|
||||
draft: false
|
||||
prerelease: true
|
||||
prerelease: true
|
||||
382
.github/workflows/reusable-check.yml
vendored
382
.github/workflows/reusable-check.yml
vendored
|
|
@ -9,18 +9,12 @@ on:
|
|||
run_unit_tests:
|
||||
type: boolean
|
||||
default: true
|
||||
run_instrumented_tests:
|
||||
run_coverage:
|
||||
type: boolean
|
||||
default: true
|
||||
run_desktop_builds:
|
||||
type: boolean
|
||||
default: true
|
||||
flavors:
|
||||
type: string
|
||||
default: '["google"]'
|
||||
api_levels:
|
||||
type: string
|
||||
default: '[35]'
|
||||
num_shards:
|
||||
type: number
|
||||
default: 1
|
||||
upload_artifacts:
|
||||
type: boolean
|
||||
default: true
|
||||
|
|
@ -42,164 +36,280 @@ on:
|
|||
GRADLE_CACHE_PASSWORD:
|
||||
required: false
|
||||
|
||||
env:
|
||||
DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }}
|
||||
DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }}
|
||||
MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }}
|
||||
GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }}
|
||||
GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }}
|
||||
# Fallback VERSION_CODE for the lint-check job itself (which computes the real
|
||||
# value from git). Downstream jobs override this with the git-derived value.
|
||||
VERSION_CODE: ${{ github.run_number }}
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
api_level: ${{ fromJson(inputs.api_levels) }}
|
||||
flavor: ${{ fromJson(inputs.flavors) }}
|
||||
env:
|
||||
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
|
||||
DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }}
|
||||
DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }}
|
||||
MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
|
||||
GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }}
|
||||
GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }}
|
||||
GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }}
|
||||
|
||||
# ── Lint & Static Analysis ──────────────────────────────────────────
|
||||
lint-check:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
timeout-minutes: 30
|
||||
outputs:
|
||||
cache_read_only: ${{ steps.cache_config.outputs.cache_read_only }}
|
||||
version_code: ${{ steps.version_code.outputs.version_code }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
filter: 'blob:none'
|
||||
submodules: true
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'jetbrains'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
build-scan-publish: true
|
||||
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
|
||||
build-scan-terms-of-use-agree: 'yes'
|
||||
add-job-summary: always
|
||||
|
||||
- name: Calculate Version Code
|
||||
id: calculate_version_code
|
||||
uses: ./.github/actions/calculate-version-code
|
||||
|
||||
- name: Determine Tasks
|
||||
id: tasks
|
||||
- name: Determine cache read-only setting
|
||||
id: cache_config
|
||||
shell: bash
|
||||
run: |
|
||||
TASKS=""
|
||||
# Only run Lint and Unit Tests on the first API level and first flavor in the matrix to save time and resources
|
||||
IS_FIRST_API=$(echo '${{ inputs.api_levels }}' | jq -r '.[0] == ${{ matrix.api_level }}')
|
||||
IS_FIRST_FLAVOR=$(echo '${{ inputs.flavors }}' | jq -r '.[0] == "${{ matrix.flavor }}"')
|
||||
|
||||
if [ "$IS_FIRST_API" = "true" ] && [ "$IS_FIRST_FLAVOR" = "true" ]; then
|
||||
[ "${{ inputs.run_lint }}" = "true" ] && TASKS="$TASKS spotlessCheck detekt "
|
||||
[ "${{ inputs.run_unit_tests }}" = "true" ] && TASKS="$TASKS testDebugUnitTest "
|
||||
if [[ "${{ github.ref }}" == "refs/heads/main" ]] || [[ "${{ github.event_name }}" == "merge_group" ]] || [[ "${{ github.ref }}" == gh-readonly-queue/* ]]; then
|
||||
echo "cache_read_only=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "cache_read_only=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
FLAVOR="${{ matrix.flavor }}"
|
||||
if [ "$IS_FIRST_API" = "true" ]; then
|
||||
if [ "$FLAVOR" = "google" ]; then
|
||||
TASKS="$TASKS assembleGoogleDebug "
|
||||
[ "${{ inputs.run_unit_tests }}" = "true" ] && TASKS="$TASKS testGoogleDebugUnitTest "
|
||||
elif [ "$FLAVOR" = "fdroid" ]; then
|
||||
TASKS="$TASKS assembleFdroidDebug "
|
||||
[ "${{ inputs.run_unit_tests }}" = "true" ] && TASKS="$TASKS testFdroidDebugUnitTest "
|
||||
fi
|
||||
fi
|
||||
|
||||
# Instrumented Test Tasks
|
||||
if [ "${{ inputs.run_instrumented_tests }}" = "true" ]; then
|
||||
[ "$IS_FIRST_FLAVOR" = "true" ] && TASKS="$TASKS connectedDebugAndroidTest "
|
||||
if [ "$FLAVOR" = "google" ]; then
|
||||
TASKS="$TASKS connectedGoogleDebugAndroidTest "
|
||||
elif [ "$FLAVOR" = "fdroid" ]; then
|
||||
TASKS="$TASKS connectedFdroidDebugAndroidTest "
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run coverage report if unit tests were executed
|
||||
if [ "${{ inputs.run_unit_tests }}" = "true" ] && [ "$IS_FIRST_API" = "true" ]; then
|
||||
if [ "$IS_FIRST_FLAVOR" = "true" ]; then
|
||||
TASKS="$TASKS koverXmlReportDebug "
|
||||
fi
|
||||
if [ "$FLAVOR" = "google" ]; then
|
||||
TASKS="$TASKS koverXmlReportGoogleDebug "
|
||||
elif [ "$FLAVOR" = "fdroid" ]; then
|
||||
TASKS="$TASKS koverXmlReportFdroidDebug "
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "tasks=$TASKS" >> $GITHUB_OUTPUT
|
||||
echo "is_first_api=$IS_FIRST_API" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Enable KVM group perms
|
||||
if: inputs.run_instrumented_tests == true
|
||||
- name: Calculate version code from git commit count
|
||||
id: version_code
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
OFFSET=$(grep '^VERSION_CODE_OFFSET=' config.properties | cut -d'=' -f2 || echo 0)
|
||||
VERSION_CODE=$((COMMIT_COUNT + OFFSET))
|
||||
echo "version_code=$VERSION_CODE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Run Check (with Emulator)
|
||||
if: inputs.run_instrumented_tests == true
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
env:
|
||||
VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }}
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
with:
|
||||
api-level: ${{ matrix.api_level }}
|
||||
arch: x86_64
|
||||
force-avd-creation: false
|
||||
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||
disable-animations: true
|
||||
script: ./gradlew ${{ steps.tasks.outputs.tasks }} -Pci=true -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true --continue --scan
|
||||
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
cache_read_only: ${{ steps.cache_config.outputs.cache_read_only }}
|
||||
|
||||
- name: Run Check (no Emulator)
|
||||
if: inputs.run_instrumented_tests == false
|
||||
env:
|
||||
VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }}
|
||||
run: ./gradlew ${{ steps.tasks.outputs.tasks }} -Pci=true -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true --continue --scan
|
||||
- name: Lint, Analysis & KMP Smoke Compile
|
||||
if: inputs.run_lint == true
|
||||
run: ./gradlew spotlessCheck detekt app:lintFdroidDebug app:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug core:api:lintDebug kmpSmokeCompile -Pci=true --continue --scan
|
||||
|
||||
- name: Upload coverage results to Codecov
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/codecov-action@v5
|
||||
- name: KMP Smoke Compile (lint skipped)
|
||||
if: inputs.run_lint == false
|
||||
run: ./gradlew kmpSmokeCompile -Pci=true --continue --scan
|
||||
|
||||
# ── Sharded Unit Tests ──────────────────────────────────────────────
|
||||
# Tests are split into 3 shards that run in parallel:
|
||||
# shard-core: core:* KMP module tests (allTests)
|
||||
# shard-feature: feature:* KMP module tests (allTests)
|
||||
# shard-app: Pure-Android/JVM tests (app, desktop, core:barcode, etc.)
|
||||
test-shards:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
timeout-minutes: 45
|
||||
needs: lint-check
|
||||
if: inputs.run_unit_tests == true
|
||||
env:
|
||||
VERSION_CODE: ${{ needs.lint-check.outputs.version_code }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard:
|
||||
- name: shard-core
|
||||
tasks: >-
|
||||
:core:ble:allTests
|
||||
:core:common:allTests
|
||||
:core:data:allTests
|
||||
:core:database:allTests
|
||||
:core:domain:allTests
|
||||
:core:model:allTests
|
||||
:core:navigation:allTests
|
||||
:core:network:allTests
|
||||
:core:prefs:allTests
|
||||
:core:repository:allTests
|
||||
:core:service:allTests
|
||||
:core:takserver:allTests
|
||||
:core:testing:allTests
|
||||
:core:ui:allTests
|
||||
kover: >-
|
||||
:core:ble:koverXmlReport
|
||||
:core:common:koverXmlReport
|
||||
:core:data:koverXmlReport
|
||||
:core:database:koverXmlReport
|
||||
:core:domain:koverXmlReport
|
||||
:core:model:koverXmlReport
|
||||
:core:navigation:koverXmlReport
|
||||
:core:network:koverXmlReport
|
||||
:core:prefs:koverXmlReport
|
||||
:core:repository:koverXmlReport
|
||||
:core:service:koverXmlReport
|
||||
:core:takserver:koverXmlReport
|
||||
:core:testing:koverXmlReport
|
||||
:core:ui:koverXmlReport
|
||||
- name: shard-feature
|
||||
tasks: >-
|
||||
:feature:connections:allTests
|
||||
:feature:firmware:allTests
|
||||
:feature:intro:allTests
|
||||
:feature:map:allTests
|
||||
:feature:messaging:allTests
|
||||
:feature:node:allTests
|
||||
:feature:settings:allTests
|
||||
kover: >-
|
||||
:feature:connections:koverXmlReport
|
||||
:feature:firmware:koverXmlReport
|
||||
:feature:intro:koverXmlReport
|
||||
:feature:map:koverXmlReport
|
||||
:feature:messaging:koverXmlReport
|
||||
:feature:node:koverXmlReport
|
||||
:feature:settings:koverXmlReport
|
||||
- name: shard-app
|
||||
tasks: >-
|
||||
:app:testFdroidDebugUnitTest
|
||||
:app:testGoogleDebugUnitTest
|
||||
:desktop:test
|
||||
:core:barcode:testFdroidDebugUnitTest
|
||||
:core:barcode:testGoogleDebugUnitTest
|
||||
kover: >-
|
||||
:app:koverXmlReportFdroidDebug
|
||||
:app:koverXmlReportGoogleDebug
|
||||
:core:barcode:koverXmlReportFdroidDebug
|
||||
:core:barcode:koverXmlReportGoogleDebug
|
||||
:desktop:koverXmlReport
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
slug: meshtastic/Meshtastic-Android
|
||||
files: "**/build/reports/kover/report*.xml"
|
||||
fetch-depth: 1
|
||||
submodules: true
|
||||
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
with:
|
||||
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
cache_read_only: ${{ needs.lint-check.outputs.cache_read_only }}
|
||||
|
||||
- name: Run Tests & Coverage (${{ matrix.shard.name }})
|
||||
run: |
|
||||
kover_tasks=""
|
||||
if [[ "${{ inputs.run_coverage }}" == "true" ]]; then
|
||||
kover_tasks="${{ matrix.shard.kover }}"
|
||||
fi
|
||||
./gradlew ${{ matrix.shard.tasks }} $kover_tasks -Pci=true --continue --scan
|
||||
|
||||
- name: Upload test results to Codecov
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v6
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
slug: meshtastic/Meshtastic-Android
|
||||
flags: ${{ matrix.shard.name }}
|
||||
fail_ci_if_error: false
|
||||
report_type: test_results
|
||||
files: "**/build/test-results/**/*.xml,**/build/outputs/androidTest-results/**/*.xml"
|
||||
files: "**/build/test-results/**/*.xml"
|
||||
|
||||
- name: Upload debug artifact
|
||||
if: ${{ steps.tasks.outputs.is_first_api == 'true' && inputs.upload_artifacts }}
|
||||
- name: Upload coverage to Codecov
|
||||
if: ${{ !cancelled() && inputs.run_coverage }}
|
||||
uses: codecov/codecov-action@v6
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
slug: meshtastic/Meshtastic-Android
|
||||
flags: ${{ matrix.shard.name }}
|
||||
fail_ci_if_error: false
|
||||
files: "**/build/reports/kover/report*.xml"
|
||||
|
||||
- name: Upload shard reports
|
||||
if: ${{ always() && inputs.upload_artifacts }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.flavor }}Debug
|
||||
path: app/build/outputs/apk/${{ matrix.flavor }}/debug/app-${{ matrix.flavor }}-debug.apk
|
||||
retention-days: 14
|
||||
name: reports-${{ matrix.shard.name }}
|
||||
path: |
|
||||
**/build/reports
|
||||
**/build/test-results
|
||||
retention-days: 7
|
||||
|
||||
# ── Android Build ────────────────────────────────────────────────────
|
||||
android-check:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
timeout-minutes: 60
|
||||
needs: lint-check
|
||||
env:
|
||||
VERSION_CODE: ${{ needs.lint-check.outputs.version_code }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
submodules: true
|
||||
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
with:
|
||||
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
cache_read_only: ${{ needs.lint-check.outputs.cache_read_only }}
|
||||
|
||||
- name: Build Android APKs
|
||||
run: ./gradlew app:assembleFdroidDebug app:assembleGoogleDebug -Pci=true --parallel --configuration-cache --continue --scan
|
||||
|
||||
- name: Upload debug artifact
|
||||
if: ${{ inputs.upload_artifacts }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: app-debug-apks
|
||||
path: app/build/outputs/apk/*/debug/*.apk
|
||||
retention-days: 7
|
||||
|
||||
- name: Report App Size
|
||||
if: always() && steps.tasks.outputs.is_first_api == 'true'
|
||||
if: always()
|
||||
run: |
|
||||
echo "### 📦 App Size Report" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### App Size Report" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Artifact | Size |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| --- | --- |" >> $GITHUB_STEP_SUMMARY
|
||||
find app/build/outputs/apk -name "*.apk" -exec du -h {} + | awk '{print "| " $2 " | " $1 " |"}' >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Upload reports
|
||||
if: ${{ always() && inputs.upload_artifacts }}
|
||||
# ── Desktop Build ───────────────────────────────────────────────────
|
||||
build-desktop:
|
||||
name: Build Desktop Debug (${{ matrix.os }})
|
||||
if: inputs.run_desktop_builds == true
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: read
|
||||
timeout-minutes: 60
|
||||
needs: lint-check
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-latest, windows-latest, ubuntu-24.04, ubuntu-24.04-arm]
|
||||
env:
|
||||
VERSION_CODE: ${{ needs.lint-check.outputs.version_code }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
submodules: true
|
||||
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
with:
|
||||
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
cache_read_only: ${{ needs.lint-check.outputs.cache_read_only }}
|
||||
|
||||
- name: Build Desktop
|
||||
run: ./gradlew :desktop:createDistributable -Pci=true --scan
|
||||
|
||||
- name: Upload Desktop artifact
|
||||
if: ${{ inputs.upload_artifacts }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: reports-${{ matrix.flavor }}-api-${{ matrix.api_level }}
|
||||
path: |
|
||||
**/build/reports
|
||||
**/build/test-results
|
||||
**/build/outputs/androidTest-results
|
||||
name: desktop-app-${{ runner.os }}-${{ runner.arch }}
|
||||
path: desktop/build/compose/binaries/main/app/
|
||||
retention-days: 7
|
||||
|
|
|
|||
28
.github/workflows/scheduled-updates.yml
vendored
28
.github/workflows/scheduled-updates.yml
vendored
|
|
@ -2,12 +2,12 @@ name: Scheduled Updates (Firmware, Hardware, Translations)
|
|||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 * * * *' # Run every hour
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
- cron: '0 */4 * * *' # Run every 4 hours (was hourly — reduced to cut cascade CI cost)
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
|
||||
jobs:
|
||||
update_assets:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
permissions:
|
||||
contents: write # To commit files and push branches
|
||||
|
|
@ -81,22 +81,11 @@ jobs:
|
|||
- name: Fix file permissions
|
||||
run: sudo chown -R $USER:$USER .
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'jetbrains'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
build-scan-publish: true
|
||||
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
|
||||
build-scan-terms-of-use-agree: 'yes'
|
||||
add-job-summary: always
|
||||
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
cache_read_only: 'false'
|
||||
|
||||
- name: Update Graphs
|
||||
run: ./gradlew graphUpdate
|
||||
|
|
@ -143,7 +132,8 @@ jobs:
|
|||
|
||||
check-workflow-status:
|
||||
name: Check Workflow Status
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04-arm
|
||||
permissions: {}
|
||||
needs:
|
||||
- update_assets
|
||||
if: always()
|
||||
|
|
|
|||
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
|
|
@ -12,7 +12,7 @@ permissions:
|
|||
jobs:
|
||||
stale_issues:
|
||||
name: Close Stale Issues
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04-arm
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
|
||||
steps:
|
||||
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
uses: actions/stale@v10.2.0
|
||||
with:
|
||||
days-before-stale: 30
|
||||
stale-issue-message: This issue hasn not had any comment or update in the last 30 days. If it is still relevant, please post update comments. If no comments are made, this issue will be closed in 7 days.
|
||||
stale-issue-message: This issue has not had any comment or update in the last 30 days. If it is still relevant, please post update comments. If no comments are made, this issue will be closed in 7 days.
|
||||
exempt-issue-labels: 'has sponsor,needs sponsor,help wanted,backlog,security issue,l10n,dependencies'
|
||||
exempt-pr-labels: 'has sponsor,needs sponsor,help wanted,backlog,security issue,l10n,dependencies'
|
||||
operations-per-run: 100
|
||||
|
|
|
|||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -37,6 +37,9 @@ keystore.properties
|
|||
/fastlane/play-store-credentials.json
|
||||
**/google-services.json
|
||||
|
||||
# Generated library definitions
|
||||
**/src/main/resources/aboutlibraries.json
|
||||
|
||||
/fastlane/report.xml
|
||||
|
||||
/build-logic/convention/build/*
|
||||
|
|
@ -48,3 +51,8 @@ wireless-install.sh
|
|||
|
||||
# Git worktrees
|
||||
.worktrees/
|
||||
/firebase-debug.log.jdk/
|
||||
firebase-debug.log
|
||||
.agent_plans/
|
||||
.agent_refs/
|
||||
.agent_artifacts/
|
||||
|
|
|
|||
1
.jdk
Symbolic link
1
.jdk
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
/home/james/.jdks/ms-17.0.18
|
||||
295
.pr5167.diff
Normal file
295
.pr5167.diff
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
diff --git a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/ApplicationCoroutineScope.kt b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/ApplicationCoroutineScope.kt
|
||||
new file mode 100644
|
||||
index 0000000000..2a27b96906
|
||||
--- /dev/null
|
||||
+++ b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/ApplicationCoroutineScope.kt
|
||||
@@ -0,0 +1,39 @@
|
||||
+/*
|
||||
+ * Copyright (c) 2026 Meshtastic LLC
|
||||
+ *
|
||||
+ * 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 org.meshtastic.core.common.di
|
||||
+
|
||||
+import kotlinx.coroutines.CoroutineScope
|
||||
+import kotlinx.coroutines.SupervisorJob
|
||||
+import org.koin.core.annotation.Single
|
||||
+import org.meshtastic.core.common.util.ioDispatcher
|
||||
+
|
||||
+/**
|
||||
+ * A process-wide [CoroutineScope] that outlives individual ViewModels and UI components.
|
||||
+ *
|
||||
+ * Use this scope for fire-and-forget cleanup work that must continue after a ViewModel's own scope has been cancelled
|
||||
+ * (for example, deleting temporary files in `onCleared()`). Backed by a [SupervisorJob] so failures in one child do not
|
||||
+ * cancel siblings, and by [ioDispatcher] so work runs off the main thread.
|
||||
+ *
|
||||
+ * Prefer scoping work to a more specific scope (like `viewModelScope`) whenever possible; this scope is an escape hatch
|
||||
+ * and should be used sparingly.
|
||||
+ */
|
||||
+interface ApplicationCoroutineScope : CoroutineScope
|
||||
+
|
||||
+@Single(binds = [ApplicationCoroutineScope::class])
|
||||
+internal class ApplicationCoroutineScopeImpl : ApplicationCoroutineScope {
|
||||
+ override val coroutineContext = SupervisorJob() + ioDispatcher
|
||||
+}
|
||||
diff --git a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
|
||||
index 231c84d401..5365ab95e2 100644
|
||||
--- a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
|
||||
+++ b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
|
||||
@@ -37,12 +37,12 @@ import androidx.lifecycle.compose.LifecycleEventEffect
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.eygraber.uri.toAndroidUri
|
||||
import com.eygraber.uri.toKmpUri
|
||||
-import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.meshtastic.core.common.gpsDisabled
|
||||
import org.meshtastic.core.common.util.CommonUri
|
||||
+import org.meshtastic.core.common.util.ioDispatcher
|
||||
import java.net.URLEncoder
|
||||
|
||||
@Composable
|
||||
@@ -146,7 +146,7 @@ actual fun rememberReadTextFromUri(): suspend (uri: CommonUri, maxChars: Int) ->
|
||||
val context = LocalContext.current
|
||||
return remember(context) {
|
||||
{ uri, maxChars ->
|
||||
- withContext(Dispatchers.IO) {
|
||||
+ withContext(ioDispatcher) {
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
val androidUri = uri.toAndroidUri()
|
||||
diff --git a/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt b/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
|
||||
index 031e1fe35d..a938f92ea6 100644
|
||||
--- a/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
|
||||
+++ b/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
|
||||
@@ -20,10 +20,10 @@ package org.meshtastic.core.ui.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import co.touchlab.kermit.Logger
|
||||
-import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.common.util.CommonUri
|
||||
+import org.meshtastic.core.common.util.ioDispatcher
|
||||
import java.awt.Desktop
|
||||
import java.awt.FileDialog
|
||||
import java.awt.Frame
|
||||
@@ -89,7 +89,7 @@ actual fun rememberOpenFileLauncher(onUriReceived: (CommonUri?) -> Unit): (mimeT
|
||||
/** JVM — Reads text from a file URI. */
|
||||
@Composable
|
||||
actual fun rememberReadTextFromUri(): suspend (uri: CommonUri, maxChars: Int) -> String? = { uri, maxChars ->
|
||||
- withContext(Dispatchers.IO) {
|
||||
+ withContext(ioDispatcher) {
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
val file = File(URI(uri.toString()))
|
||||
diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
|
||||
index dc1c459716..f8ff9fcac8 100644
|
||||
--- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
|
||||
+++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
|
||||
@@ -35,6 +35,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.koin.core.annotation.KoinViewModel
|
||||
+import org.meshtastic.core.common.di.ApplicationCoroutineScope
|
||||
import org.meshtastic.core.common.util.CommonUri
|
||||
import org.meshtastic.core.common.util.safeCatching
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
@@ -91,6 +92,7 @@ class FirmwareUpdateViewModel(
|
||||
private val firmwareUpdateManager: FirmwareUpdateManager,
|
||||
private val usbManager: FirmwareUsbManager,
|
||||
private val fileHandler: FirmwareFileHandler,
|
||||
+ private val applicationScope: ApplicationCoroutineScope,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow<FirmwareUpdateState>(FirmwareUpdateState.Idle)
|
||||
@@ -124,12 +126,10 @@ class FirmwareUpdateViewModel(
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
- // viewModelScope is already cancelled when onCleared() runs, so launch cleanup in a
|
||||
- // standalone scope. SupervisorJob prevents the coroutine from propagating failures to a
|
||||
- // shared parent, and NonCancellable on the launch keeps cleanup running even if the scope
|
||||
- // is cancelled concurrently.
|
||||
- @OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class)
|
||||
- kotlinx.coroutines.GlobalScope.launch(NonCancellable) {
|
||||
+ // viewModelScope is already cancelled when onCleared() runs, so launch cleanup on the
|
||||
+ // application-wide scope (SupervisorJob + ioDispatcher). NonCancellable keeps cleanup
|
||||
+ // running even if something tries to cancel it mid-flight.
|
||||
+ applicationScope.launch(NonCancellable) {
|
||||
tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile)
|
||||
}
|
||||
}
|
||||
diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt
|
||||
index 4c48a1ced5..030d84effd 100644
|
||||
--- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt
|
||||
+++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt
|
||||
@@ -108,6 +108,7 @@ class FirmwareUpdateIntegrationTest {
|
||||
firmwareUpdateManager,
|
||||
usbManager,
|
||||
fileHandler,
|
||||
+ TestApplicationCoroutineScope(testDispatcher),
|
||||
)
|
||||
|
||||
@Test
|
||||
diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt
|
||||
index 7032ed4088..a8eddff838 100644
|
||||
--- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt
|
||||
+++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt
|
||||
@@ -124,6 +124,7 @@ class FirmwareUpdateViewModelTest {
|
||||
firmwareUpdateManager,
|
||||
usbManager,
|
||||
fileHandler,
|
||||
+ TestApplicationCoroutineScope(testDispatcher),
|
||||
)
|
||||
|
||||
@Test
|
||||
diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/TestApplicationCoroutineScope.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/TestApplicationCoroutineScope.kt
|
||||
new file mode 100644
|
||||
index 0000000000..3ef5c44ef4
|
||||
--- /dev/null
|
||||
+++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/TestApplicationCoroutineScope.kt
|
||||
@@ -0,0 +1,26 @@
|
||||
+/*
|
||||
+ * Copyright (c) 2026 Meshtastic LLC
|
||||
+ *
|
||||
+ * 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 org.meshtastic.feature.firmware
|
||||
+
|
||||
+import kotlinx.coroutines.CoroutineDispatcher
|
||||
+import kotlinx.coroutines.CoroutineScope
|
||||
+import kotlinx.coroutines.SupervisorJob
|
||||
+import org.meshtastic.core.common.di.ApplicationCoroutineScope
|
||||
+
|
||||
+internal class TestApplicationCoroutineScope(dispatcher: CoroutineDispatcher) :
|
||||
+ ApplicationCoroutineScope,
|
||||
+ CoroutineScope by CoroutineScope(SupervisorJob() + dispatcher)
|
||||
diff --git a/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt
|
||||
index acb1545bdd..23a0d03ab2 100644
|
||||
--- a/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt
|
||||
+++ b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt
|
||||
@@ -116,6 +116,7 @@ class FirmwareUpdateViewModelFileTest {
|
||||
firmwareUpdateManager,
|
||||
usbManager,
|
||||
fileHandler,
|
||||
+ TestApplicationCoroutineScope(testDispatcher),
|
||||
)
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
|
||||
index c251b4d5ef..315ad1da85 100644
|
||||
--- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
|
||||
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
|
||||
@@ -27,6 +27,7 @@ import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
+import org.meshtastic.core.common.util.ioDispatcher
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.debug_export_failed
|
||||
import org.meshtastic.core.resources.debug_export_success
|
||||
@@ -48,7 +49,7 @@ actual fun rememberLogExporter(logsProvider: suspend () -> List<DebugViewModel.U
|
||||
}
|
||||
|
||||
private suspend fun exportAllLogsToUri(context: Context, targetUri: Uri, logs: List<DebugViewModel.UiMeshLog>) =
|
||||
- withContext(Dispatchers.IO) {
|
||||
+ withContext(ioDispatcher) {
|
||||
try {
|
||||
if (logs.isEmpty()) {
|
||||
withContext(Dispatchers.Main) { context.showToast(Res.string.debug_export_failed, "No logs to export") }
|
||||
diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
|
||||
index 9afde85e5f..a28a576788 100644
|
||||
--- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
|
||||
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
|
||||
@@ -24,9 +24,9 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import co.touchlab.kermit.Logger
|
||||
-import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
+import org.meshtastic.core.common.util.ioDispatcher
|
||||
|
||||
@Composable
|
||||
actual fun rememberDataPackageExporter(dataPackageProvider: suspend () -> ByteArray): (fileName: String) -> Unit {
|
||||
@@ -41,7 +41,7 @@ actual fun rememberDataPackageExporter(dataPackageProvider: suspend () -> ByteAr
|
||||
return { fileName -> exportLauncher.launch(fileName) }
|
||||
}
|
||||
|
||||
-private suspend fun exportZipToUri(context: Context, targetUri: Uri, data: ByteArray) = withContext(Dispatchers.IO) {
|
||||
+private suspend fun exportZipToUri(context: Context, targetUri: Uri, data: ByteArray) = withContext(ioDispatcher) {
|
||||
try {
|
||||
context.contentResolver.openOutputStream(targetUri)?.use { os -> os.write(data) }
|
||||
Logger.i { "TAK data package exported successfully to $targetUri" }
|
||||
diff --git a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
|
||||
index 5b63cc90a3..a9a7285593 100644
|
||||
--- a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
|
||||
+++ b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
|
||||
@@ -19,9 +19,9 @@ package org.meshtastic.feature.settings.debugging
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import co.touchlab.kermit.Logger
|
||||
-import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
+import org.meshtastic.core.common.util.ioDispatcher
|
||||
import java.awt.FileDialog
|
||||
import java.awt.Frame
|
||||
import java.io.File
|
||||
@@ -41,7 +41,7 @@ actual fun rememberLogExporter(logsProvider: suspend () -> List<DebugViewModel.U
|
||||
return@launch
|
||||
}
|
||||
|
||||
- withContext(Dispatchers.IO) {
|
||||
+ withContext(ioDispatcher) {
|
||||
// Run file dialog to ask user where to save
|
||||
val fileDialog = FileDialog(null as Frame?, "Export Logs", FileDialog.SAVE)
|
||||
fileDialog.file = fileName
|
||||
diff --git a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
|
||||
index 9fb71379fc..bfbb85bc0d 100644
|
||||
--- a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
|
||||
+++ b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
|
||||
@@ -19,9 +19,9 @@ package org.meshtastic.feature.settings.tak
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import co.touchlab.kermit.Logger
|
||||
-import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
+import org.meshtastic.core.common.util.ioDispatcher
|
||||
import java.awt.FileDialog
|
||||
import java.awt.Frame
|
||||
import java.io.File
|
||||
@@ -44,7 +44,7 @@ actual fun rememberDataPackageExporter(dataPackageProvider: suspend () -> ByteAr
|
||||
if (directory != null && file != null) {
|
||||
val targetFile = File(directory, file)
|
||||
val data = dataPackageProvider()
|
||||
- withContext(Dispatchers.IO) { targetFile.writeBytes(data) }
|
||||
+ withContext(ioDispatcher) { targetFile.writeBytes(data) }
|
||||
Logger.i { "TAK data package exported successfully to ${targetFile.absolutePath}" }
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
3.4.8
|
||||
3.4.9
|
||||
|
|
|
|||
66
.skills/code-review/SKILL.md
Normal file
66
.skills/code-review/SKILL.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# Skill: Code Review
|
||||
|
||||
## Description
|
||||
Perform comprehensive code reviews for `Meshtastic-Android`, ensuring changes adhere to KMP architecture, Kotlin Multiplatform conventions, MAD standards, and CMP best practices.
|
||||
|
||||
## Code Review Checklist
|
||||
|
||||
When reviewing code, meticulously verify the following categories. Flag any deviations and propose the canonical project pattern as a fix.
|
||||
|
||||
### 1. KMP Architecture & Source Set Boundaries
|
||||
- [ ] **No Platform Bleed:** Ensure absolutely no `java.*` or `android.*` imports exist in `commonMain` source sets.
|
||||
- [ ] **KMP Native Alternatives:** Verify the use of KMP alternatives for standard JVM libraries:
|
||||
- `java.util.concurrent.locks.*` -> `kotlinx.coroutines.sync.Mutex`
|
||||
- `java.util.concurrent.ConcurrentHashMap` -> `atomicfu` or Mutex-guarded `mutableMapOf()`
|
||||
- `java.io.*` -> `Okio` (`BufferedSource`/`BufferedSink`)
|
||||
- `java.util.Locale` -> Kotlin `uppercase()`/`lowercase()` (purged from `commonMain`)
|
||||
- [ ] **Coroutine Safety:** Use `safeCatching {}` from `core:common` instead of `runCatching {}` in coroutine/suspend contexts. `runCatching` silently swallows `CancellationException`, breaking structured concurrency. Keep `runCatching` only in cleanup/teardown code (abort, close, eviction). Use `kotlinx.coroutines.CancellationException` (not `kotlin.coroutines.cancellation.CancellationException`).
|
||||
- [ ] **Shared Helpers:** If `androidMain` and `jvmMain` contain identical pure-Kotlin logic, mandate extracting it to a shared function in `commonMain`.
|
||||
- [ ] **File Naming Conflicts:** For `expect`/`actual` declarations, ensure files sharing the same package namespace have distinct names (e.g., keep `expect` in `LogExporter.kt` and shared helpers in `LogFormatter.kt`) to avoid duplicate class errors on the JVM target.
|
||||
- [ ] **Interface & DI Over `expect`/`actual`:** Check that `expect`/`actual` is reserved for small platform primitives. Interfaces + DI should be preferred for larger capabilities.
|
||||
|
||||
### 2. UI & Compose Multiplatform (CMP)
|
||||
- [ ] **Compose Multiplatform Resources:** Ensure NO hardcoded strings. Must use `core:resources` (e.g., `stringResource(Res.string.key)` or asynchronous `getStringSuspend(Res.string.key)` for ViewModels/Coroutines). NEVER use blocking `getString()` in a coroutine.
|
||||
- [ ] **String Formatting:** CMP only supports `%N$s` and `%N$d`. Flag any float formats (`%N$.1f`) in Compose string resources; they must be pre-formatted using `NumberFormatter.format()` from `core:common`. Use `MetricFormatter` for metric-specific displays (temperature, voltage, current, percent, humidity, pressure, SNR, RSSI).
|
||||
- [ ] **Centralized Dialogs & Alerts:** Flag inline alert-rendering logic. Mandate the use of `AlertHost(alertManager)` or `SharedDialogs` from `core:ui/commonMain`.
|
||||
- [ ] **Placeholders:** Require `PlaceholderScreen(name)` from `core:ui/commonMain` for unimplemented desktop/JVM features. No inline placeholders in feature modules.
|
||||
- [ ] **Adaptive Layouts:** Verify use of `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` to support desktop/tablet breakpoints (≥ 1200dp).
|
||||
|
||||
### 3. Navigation & State
|
||||
- [ ] **Shared Navigation Graphs:** Feature navigation graphs must be defined as extension functions on `EntryProviderScope<NavKey>` in `commonMain` (e.g., `fun EntryProviderScope<NavKey>.settingsGraph(...)`). Flag any graphs defined in platform-specific source sets.
|
||||
- [ ] **Navigation Host:** Ensure `MeshtasticNavDisplay` (from `core:ui/commonMain`) is used as the host instead of invoking `NavDisplay` directly. Host modules should not configure `entryDecorators` themselves.
|
||||
- [ ] **ViewModel Scoping:** ViewModels obtained via `koinViewModel()` must be inside `entry<T>` blocks to correctly tie to the backstack lifetime.
|
||||
|
||||
### 4. Dependency Injection (Koin Annotations)
|
||||
- [ ] **Annotation Usage:** Ensure Koin is configured via annotations (`@Single`, `@Factory`, `@KoinViewModel`).
|
||||
- [ ] **Root Assembly:** Confirm that the root Koin DI graph is only assembled in host shells (`app` and `desktop`).
|
||||
|
||||
### 5. Networking, DB & I/O
|
||||
- [ ] **Ktor Strictly:** Check that Ktor is used for all HTTP networking. Flag and reject any usage of OkHttp.
|
||||
- [ ] **HTTP Configuration:** Verify timeouts and base URLs use `HttpClientDefaults` from `core:network`. Never hardcode timeouts in feature modules. `DefaultRequest` sets the base URL; feature API services use relative paths.
|
||||
- [ ] **Image Loading (Coil):** Coil must use `coil-network-ktor3` in host modules. Feature modules should ONLY depend on `libs.coil` (coil-compose) and never configure fetchers.
|
||||
- [ ] **Room KMP:** Ensure `factory = { MeshtasticDatabaseConstructor.initialize() }` is used in `Room.databaseBuilder`. DAOs and Entities must reside in `commonMain`.
|
||||
- [ ] **Room Patterns:** Verify use of `@Upsert` for insert-or-update logic. Check for `LIMIT 1` on single-row queries. Flag N+1 query patterns (loops calling single-row queries) — batch with chunked `WHERE IN` instead.
|
||||
- [ ] **Bluetooth (BLE):** All Bluetooth communication must be routed through `core:ble` using Kable abstractions.
|
||||
|
||||
### 6. Dependency Catalog Aliases
|
||||
- [ ] **JetBrains vs. AndroidX:**
|
||||
- In `commonMain`: Must use `jetbrains-*` aliases (e.g., `jetbrains-lifecycle-*`, `jetbrains-navigation3-ui`).
|
||||
- In `androidMain`: Can use `androidx-*` or `jetbrains-*` as appropriate, but do not mix them up in `commonMain`.
|
||||
- [ ] **Compose Multiplatform:** Ensure `compose-multiplatform-*` aliases are used instead of plain `androidx.compose` in all KMP modules.
|
||||
|
||||
### 7. Testing
|
||||
- [ ] **Test Placement:** New Compose UI tests must go in `commonTest` using `runComposeUiTest {}` from `androidx.compose.ui.test.v2` (not the deprecated v1 `androidx.compose.ui.test` package) + `kotlin.test.Test`. Do not add `androidTest` (instrumented) tests.
|
||||
- [ ] **Shared Test Utilities:** Test fakes, doubles, and utilities should be placed in `core:testing`.
|
||||
- [ ] **Libraries:** Verify usage of `Turbine` for Flow testing, `Kotest` for property-based testing, and `Mokkery` for mocking.
|
||||
- [ ] **Robolectric Configuration:** Check that Compose UI tests running via Robolectric on JVM are pinned to `@Config(sdk = [34])` to prevent Java 21 / SDK 35 compatibility issues.
|
||||
|
||||
### 8. ProGuard / R8 Rules
|
||||
- [ ] **New Dependencies:** If a new reflection-heavy dependency is added (DI, serialization, JNI, ServiceLoader), verify keep rules exist in **both** `app/proguard-rules.pro` (R8) and `desktop/proguard-rules.pro` (ProGuard). The two files must stay aligned.
|
||||
- [ ] **Release Smoke-Test:** For dependency or ProGuard rule changes, verify `assembleRelease` and `./gradlew :desktop:runRelease` succeed.
|
||||
|
||||
## Review Output Guidelines
|
||||
1. **Be Specific & Constructive:** Provide exact file references and code snippets illustrating the required project pattern.
|
||||
2. **Reference the Docs:** Cite `AGENTS.md` and project architecture playbooks to justify change requests (e.g., "Per AGENTS.md, `java.io.*` cannot be used in `commonMain`; please migrate to Okio").
|
||||
3. **Enforce Build Health:** Remind authors to run `./gradlew test allTests` locally to verify changes, especially since KMP `test` tasks are ambiguous.
|
||||
4. **Praise Good Patterns:** Acknowledge correct usage of complex architecture requirements, like proper Navigation 3 scene transitions or elegant `commonMain` helper extractions.
|
||||
61
.skills/compose-ui/SKILL.md
Normal file
61
.skills/compose-ui/SKILL.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Skill: Compose Multiplatform (CMP) UI
|
||||
|
||||
## Description
|
||||
Guidelines for building shared UI, adaptive layouts, and handling strings/resources in Meshtastic-Android. The codebase uses Material 3 Adaptive.
|
||||
|
||||
## 1. UI Components & Layouts
|
||||
- **Material 3 / Adaptive:** Use `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` to support Large (1200dp) and XL (1600dp) breakpoints. Investigate 3-pane "Power User" scenes using Navigation 3 Scenes and draggable dividers for desktop/tablets.
|
||||
- **Dialogs & Alerts:** Use centralized components like `AlertHost(alertManager)` from `core:ui/commonMain`. Do NOT trigger alerts inline or duplicate alert logic. Use `SharedDialogs(uiViewModel)` for general popups.
|
||||
- **Placeholders:** Use `PlaceholderScreen(name)` from `core:ui/commonMain` for unimplemented desktop/JVM features.
|
||||
- **Theme Picker:** Use `ThemePickerDialog` from `feature:settings/commonMain`.
|
||||
- **Platform Implementations:** Inject platform-specific behavior (e.g., Map providers) via `CompositionLocal` from the `app` or `desktop` shells. Do not tightly couple Google Maps/osmdroid dependencies to `commonMain`.
|
||||
|
||||
## 2. Strings & Resources
|
||||
- **Multiplatform Resources:** MUST use `core:resources` (e.g., `stringResource(Res.string.your_key)`). Never use hardcoded strings.
|
||||
- **ViewModels/Coroutines:** Use the asynchronous `getStringSuspend(Res.string.your_key)`. NEVER use blocking `getString()` in a coroutine context.
|
||||
- **Formatting Constraints:** CMP `stringResource` only supports `%N$s` (string) and `%N$d` (integer).
|
||||
- **No Float formatting:** Formats like `%N$.1f` pass through unsubstituted. Pre-format in Kotlin using `NumberFormatter.format(value, decimalPlaces)` from `core:common` and pass as a string argument (`%N$s`):
|
||||
```kotlin
|
||||
val formatted = NumberFormatter.format(batteryLevel, 1) // "73.5"
|
||||
stringResource(Res.string.battery_percent, formatted) // uses %1$s
|
||||
```
|
||||
- **Percent Literals:** Use bare `%` (not `%%`) for literal percent signs in CMP-consumed strings.
|
||||
|
||||
### String Formatting Decision Tree
|
||||
Choose the right tool for the job:
|
||||
|
||||
| Scenario | Tool | Example |
|
||||
|----------|------|---------|
|
||||
| **Metric display** (temp, voltage, %, signal) | `MetricFormatter.*` | `MetricFormatter.temperature(25.0f, isFahrenheit)` → `"77.0°F"` |
|
||||
| **Simple number + unit** | `NumberFormatter` + interpolation | `"${NumberFormatter.format(val, 1)} dB"` |
|
||||
| **Localized template from strings.xml** | `stringResource(Res.string.key, preFormattedArgs)` | `stringResource(Res.string.battery, formatted)` |
|
||||
| **Non-composable template** (notifications, plain functions) | `formatString(template, args)` | `formatString(template, label, value)` |
|
||||
| **Hex formatting** | `formatString` | `formatString("!%08x", nodeNum)` |
|
||||
| **Date/time** | `DateFormatter` | `DateFormatter.format(instant)` |
|
||||
|
||||
**Rules:**
|
||||
1. **NEVER use `%.Nf` in strings.xml** — CMP cannot substitute them. Use `%N$s` and pre-format floats.
|
||||
2. **Prefer `MetricFormatter`** over scattered `formatString("%.1f°C", temp)` calls.
|
||||
3. **`formatString` (pure Kotlin)** is a pure-Kotlin `commonMain` implementation for: hex formats, multi-arg templates fetched at runtime, and chart axis formatters. Located in `core:common` `Formatter.kt`.
|
||||
4. **`NumberFormatter`** always uses `.` as decimal separator — intentional for mesh networking precision.
|
||||
|
||||
- **Workflow to Add a String:**
|
||||
1. Add to `core/resources/src/commonMain/composeResources/values/strings.xml`.
|
||||
2. Use the generated `org.meshtastic.core.resources.<key>` symbol.
|
||||
3. Validate UI presentation.
|
||||
|
||||
## 3. Tooling & Capabilities
|
||||
- **Image Loading:** Use `libs.coil` (Coil Compose) in feature modules. Configuration/Networking for Coil (`coil-network-ktor3`) happens strictly in the `app` and `desktop` host modules.
|
||||
- **QR Codes:** Use `rememberQrCodePainter` from `core:ui/commonMain` powered by `qrcode-kotlin`. No ZXing or Android Bitmap APIs in shared code.
|
||||
|
||||
## 4. Compose Previews
|
||||
- **Preview in commonMain:** CMP 1.11+ supports `@Preview` in `commonMain` via `compose-multiplatform-ui-tooling-preview`. Place preview functions alongside their composables.
|
||||
- **Import:** Use `androidx.compose.ui.tooling.preview.Preview`. The JetBrains-prefixed import (`org.jetbrains.compose.ui.tooling.preview.Preview`) is deprecated.
|
||||
|
||||
## 5. Dialog & State Patterns
|
||||
- **Dialog State Preservation:** Use `rememberSaveable` for dialog state (search queries, selected tabs, expanded flags) to preserve across configuration changes. Boolean and String types are auto-saveable — no custom `Saver` needed.
|
||||
|
||||
## Reference Anchors
|
||||
- **Shared Strings:** `core/resources/src/commonMain/composeResources/values/strings.xml`
|
||||
- **Platform abstraction contract:** `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/MapViewProvider.kt`
|
||||
- **Provider wiring:** `app/src/main/kotlin/org/meshtastic/app/MainActivity.kt`
|
||||
41
.skills/implement-feature/SKILL.md
Normal file
41
.skills/implement-feature/SKILL.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Skill: Implement a Feature
|
||||
|
||||
## Description
|
||||
A step-by-step workflow for implementing a new feature in the Meshtastic-Android codebase, ensuring KMP compatibility and proper architecture.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Update Dependencies & Aliases
|
||||
- Check `gradle/libs.versions.toml` before adding libraries.
|
||||
- Use `jetbrains-*` aliases for lifecycle/navigation/adaptive dependencies in `commonMain`.
|
||||
- Use `compose-multiplatform-*` aliases for CMP dependencies.
|
||||
|
||||
### 2. Define the State & ViewModels
|
||||
- Follow MVI/UDF patterns.
|
||||
- Extend shared ViewModel logic in `feature/<name>/src/commonMain/kotlin/org/meshtastic/feature/<name>/<Name>ViewModel.kt`.
|
||||
- Use `stateInWhileSubscribed` (from `core:ui`) for sharing state flows.
|
||||
- Keep the ViewModel free of Android framework dependencies.
|
||||
|
||||
### 3. Build the UI
|
||||
- Use Jetpack Compose Multiplatform (CMP).
|
||||
- Define strings in `core:resources` (see the `compose-ui` skill).
|
||||
- Support adaptive layouts (Large/XL breakpoints).
|
||||
|
||||
### 4. Wire Navigation & DI
|
||||
- Define typed route objects in `core:navigation`.
|
||||
- Export the navigation graph as an extension function on `EntryProviderScope<NavKey>` in `commonMain` (e.g., `fun EntryProviderScope<NavKey>.myFeatureGraph()`).
|
||||
- Add the required DI bindings via Koin Annotations (`@Factory`, `@Single`, `@KoinViewModel`) in `commonMain`.
|
||||
- **CRITICAL:** Ensure the module is registered in the app root graphs (`AppKoinModule.kt`, `DesktopKoinModule.kt`) and the navigation is injected into the root entry provider in the host shell.
|
||||
|
||||
### 5. Validate Platform Separation
|
||||
- If you need a platform-specific API (like camera or specific mapping SDK), define an interface in `commonMain`, implement it in the host shell, and inject it via `CompositionLocal` or Koin.
|
||||
|
||||
### 6. Verify Locally
|
||||
- Run the baseline checks (see `testing-ci` skill):
|
||||
```bash
|
||||
./gradlew spotlessCheck spotlessApply detekt assembleDebug test allTests
|
||||
```
|
||||
- If the feature adds a new reflection-heavy dependency, add keep rules to **both** `app/proguard-rules.pro` and `desktop/proguard-rules.pro`, then verify release builds:
|
||||
```bash
|
||||
./gradlew assembleFdroidRelease :desktop:runRelease
|
||||
```
|
||||
61
.skills/kmp-architecture/SKILL.md
Normal file
61
.skills/kmp-architecture/SKILL.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Skill: KMP Architecture & Source-Set Bridging
|
||||
|
||||
## Description
|
||||
Guidelines on managing Kotlin Multiplatform (KMP) source-sets, expected abstractions, networking, database, and platform integration rules.
|
||||
|
||||
## 1. Source-Set Boundaries
|
||||
- **`commonMain`:** All business logic, DB entities, API network logic, ViewModels, and UI rendering. NO `java.*` or `android.*` imports.
|
||||
- **`androidMain`:** Android framework integration (`Context`, system services, NFC hardware, BLE Android bindings).
|
||||
- **`jvmMain` / `jvmAndroidMain`:** Shared JVM code between Android and Desktop. Uses the `meshtastic.kmp.jvm.android` convention plugin to bridge `jvm` and `android` source sets without manual `dependsOn` hacks.
|
||||
- **`app` / `desktop`:** Host shells. Responsible for Koin DI root wiring, `MainKoinModule`, host-level UI themes, and running the `MeshtasticNavDisplay`.
|
||||
|
||||
## 2. Bridging Strategies
|
||||
- **Interface + DI (Preferred):** Expose an interface in `core:repository` or `core:ui` (e.g. `LocationRepository`, `MapViewProvider`), implement it in `androidMain` or the host `app`, and bind it via Koin or `CompositionLocal`.
|
||||
- **`expect`/`actual` (Restricted):** Use only when a platform API cannot be abstracted cleanly (e.g. low-level File I/O mappings, `uppercase()` Locale helpers). Avoid deep class hierarchies using `expect`/`actual`.
|
||||
- **Naming:** Keep `expect` in `FileIo.kt`, but put shared helpers in `FileIoUtils.kt` to prevent JVM duplicate class errors.
|
||||
- **Shared Helpers:** Do not duplicate pure Kotlin logic between `androidMain` and `jvmMain`. Extract to a `commonMain` helper.
|
||||
|
||||
## 3. Core Libraries & Constraints
|
||||
- **Concurrency:** `kotlinx.coroutines`. Use `org.meshtastic.core.common.util.ioDispatcher` over `Dispatchers.IO` directly. Inject `CoroutineDispatchers` from `core:di` into classes that need dispatchers — never reference `Dispatchers.IO`/`Main`/`Default` directly in business logic.
|
||||
- **Error Handling:** Use `safeCatching {}` from `core:common` instead of `runCatching {}` in coroutine/suspend contexts. `runCatching` swallows `CancellationException`, breaking structured concurrency. Keep `runCatching` only in cleanup/teardown code (abort, close, eviction loops).
|
||||
- **Standard Library Replacements:**
|
||||
- `ConcurrentHashMap` -> `atomicfu` or Mutex-guarded `mutableMapOf()`.
|
||||
- `java.util.concurrent.locks.*` -> `kotlinx.coroutines.sync.Mutex`.
|
||||
- `java.io.*` -> `Okio` (`BufferedSource`/`BufferedSink`).
|
||||
- **Networking:** Pure **Ktor**. No OkHttp. Ktor `Logging` plugin for debugging.
|
||||
- **HTTP Configuration:** Use `HttpClientDefaults` from `core:network` for shared base URL (`API_BASE_URL`), timeouts, and retry constants. Both Android (`NetworkModule`) and Desktop (`DesktopKoinModule`) HttpClient instances must use these. Feature API services use relative paths; `DefaultRequest` sets the base URL.
|
||||
- **BLE:** Route through `core:ble` using **Kable**.
|
||||
- **Room KMP:** Use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder`.
|
||||
|
||||
## 4. Hierarchy & Source-Set Conventions
|
||||
- **Hierarchy template first:** Prefer Kotlin's default hierarchy template and convention plugins over manual `dependsOn(...)` graphs. Manual source-set wiring should be reserved for cases the template cannot model.
|
||||
- **`expect`/`actual` restraint:** Prefer interfaces + DI for platform capabilities; use `expect`/`actual` for small unavoidable platform primitives. Avoid broad expect/actual class hierarchies when an interface-based boundary is sufficient.
|
||||
- **Shared helpers over duplicated lambdas:** When `androidMain` and `jvmMain` contain identical pure-Kotlin logic (formatting, action dispatch, validation), extract to `commonMain`. Examples: `formatLogsTo()`, `handleNodeAction()`, `findNodeByNameSuffix()`, `MeshtasticAppShell`, `BaseRadioTransportFactory`.
|
||||
|
||||
## 5. Dependency Catalog Aliases
|
||||
- **JetBrains fork aliases:** Version catalog aliases for JetBrains-forked AndroidX artifacts use the `jetbrains-*` prefix (e.g., `jetbrains-lifecycle-runtime-compose`, `jetbrains-navigation3-ui`). Plain `androidx-*` aliases are true Google AndroidX artifacts. Never mix them up in `commonMain`.
|
||||
- **Compose Multiplatform:** Version catalog aliases for Compose Multiplatform artifacts use the `compose-multiplatform-*` prefix (e.g., `compose-multiplatform-material3`, `compose-multiplatform-foundation`). Never use plain `androidx.compose` dependencies in `commonMain`.
|
||||
- **Dependencies:** Always check `gradle/libs.versions.toml` before assuming a library is available.
|
||||
|
||||
## 6. I/O & Serialization
|
||||
- **Okio standard:** This project standardizes on Okio (`BufferedSource`/`BufferedSink`). JetBrains recommends `kotlinx-io` (built on Okio), but this project has not migrated. Do not introduce `kotlinx-io` without an explicit decision.
|
||||
- **Room KMP:** Always use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder` and `inMemoryDatabaseBuilder`. DAOs and Entities reside in `commonMain`.
|
||||
- **Room Patterns:**
|
||||
- Use `@Upsert` for insert-or-update operations instead of manual `INSERT OR IGNORE` + `UPDATE` logic.
|
||||
- Use `LIMIT 1` on `@Query` methods that expect a single row.
|
||||
- Prevent N+1 queries: batch operations with `@Upsert fun putAll(items: List<T>)` or chunked `WHERE IN` queries (chunk size ≤ 999 to respect SQLite bind parameter limit).
|
||||
|
||||
## 7. Build-Logic Conventions
|
||||
- In `build-logic/convention`, prefer lazy Gradle configuration (`configureEach`, `withPlugin`, provider APIs). Avoid `afterEvaluate` in convention plugins unless there is no viable lazy alternative.
|
||||
|
||||
## 8. Onboarding a New Target (Desktop/iOS)
|
||||
1. Ensure all new logic compiles against the KMP core (`jvm()`, `iosArm64()`, etc.).
|
||||
2. Do not use platform-specific constructs in `commonMain` or you break the iOS/Desktop builds.
|
||||
3. Test using `kmpSmokeCompile` to verify cross-platform compilation.
|
||||
4. For desktop wiring, copy the pattern in `desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt` and use `NoopStubs.kt` to temporarily mock missing platform implementations.
|
||||
|
||||
## Reference Anchors
|
||||
- **Shared Okio I/O:** `core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCase.kt`
|
||||
- **Desktop DI Stubs:** `desktop/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt`
|
||||
- **Version Catalog:** `gradle/libs.versions.toml`
|
||||
- **Convention Plugins:** `build-logic/convention/`
|
||||
56
.skills/navigation-and-di/SKILL.md
Normal file
56
.skills/navigation-and-di/SKILL.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Skill: DI and Navigation 3 Architecture
|
||||
|
||||
## Description
|
||||
This skill covers dependency injection (Koin Annotations 4.2.x) and JetBrains Navigation 3 (1.1.x) architecture, constraints, and anti-patterns within the Meshtastic-Android KMP codebase.
|
||||
|
||||
## Dependency Injection (Koin)
|
||||
|
||||
### Guidelines
|
||||
1. **Annotations First:** Use `@Module`, `@ComponentScan`, and `@KoinViewModel` annotations directly in `commonMain` shared modules to encapsulate dependency graphs per feature.
|
||||
2. **App Root Assembly:** Don't assume feature/core `@Module` classes are active automatically. Ensure they are included by the app root module (`@Module(includes = [...])`) in `app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt` and `desktop/.../DesktopKoinModule.kt`.
|
||||
3. **No Platform Bleed:** Don't put Android framework dependencies (`Context`, `Activity`, `Application`) into shared `commonMain` business logic. Inject interfaces instead.
|
||||
4. **Resolution:** Resolve app-layer wrappers via `koinViewModel()` or injected bindings within Compose navigation graphs.
|
||||
|
||||
### Anti-Patterns
|
||||
- **A1 Module Compile Safety:** Do **not** enable `compileSafety`. It is a single boolean that enables A1 per-module checks — there is no separate A3 full-graph mode. Runtime graph verification is handled by `KoinVerificationTest` and `DesktopKoinTest` instead.
|
||||
- **Default Parameters:** Do **not** expect Koin to inject default parameters automatically. The K2 plugin's `skipDefaultValues = true` behavior skips parameters with default Kotlin values.
|
||||
|
||||
### Koin Startup Pattern (K2 Compiler Plugin)
|
||||
The project uses the **K2 Compiler Plugin** (`koin-compiler-plugin`, not KSP). The canonical startup uses the plugin's typed `startKoin<T>()` stub, which the plugin transforms at compile time via IR:
|
||||
```kotlin
|
||||
// Bootstrap class — separate from @Module, references the root module graph
|
||||
@KoinApplication(modules = [AppKoinModule::class])
|
||||
object AndroidKoinApp
|
||||
|
||||
// In Application.onCreate()
|
||||
startKoin<AndroidKoinApp> {
|
||||
androidContext(this@MeshUtilApplication)
|
||||
workManagerFactory()
|
||||
}
|
||||
```
|
||||
- `@KoinApplication` goes on a **dedicated bootstrap object**, not on a `@Module` class.
|
||||
- `startKoin<T>()` (from `org.koin.plugin.module.dsl`) is a compiler plugin stub — if the plugin isn't applied, it throws `NotImplementedError`.
|
||||
- `stopKoin()` uses the standard runtime API (`org.koin.core.context.stopKoin`).
|
||||
- `compileSafety` must stay **disabled** — it enables A1 per-module checks that break our inverted-dependency architecture. There is no separate A3 full-graph flag.
|
||||
|
||||
## Navigation 3
|
||||
|
||||
### Guidelines
|
||||
1. **Types:** Use Navigation 3 types consistently (`NavKey`, `NavBackStack`, `EntryProviderScope`).
|
||||
2. **Typed Routes:** Keep route definitions in `core:navigation/src/commonMain/.../Routes.kt` as `@Serializable sealed interface` hierarchies. Don't use ad-hoc strings.
|
||||
3. **Graph Assembly:** Define feature navigation graphs as extension functions on `EntryProviderScope<NavKey>` in `commonMain` (e.g., `fun EntryProviderScope<NavKey>.settingsGraph(backStack)`).
|
||||
4. **Host Integration:** Use `MeshtasticNavDisplay` (from `core:ui/commonMain`) as the Navigation 3 host. Do not configure decorators manually inside feature modules.
|
||||
5. **Back Handlers:** Use `NavigationBackHandler` from `androidx.navigationevent:navigationevent-compose` for back gestures in multiplatform code. Do not use Android's `BackHandler`.
|
||||
6. **Deep Links:** Use `DeepLinkRouter.route()` in `core:navigation` to synthesize typed backstacks from RESTful paths.
|
||||
|
||||
### Anti-Patterns
|
||||
- **Single Backstack for Multiple Tabs:** Do **not** use a single `NavBackStack` list for multiple tabs. Use `MultiBackstack` (from `core:navigation`).
|
||||
- **Decorator Reuse Across Tabs:** Do **not** reuse the same `NavEntryDecorator` instances across different backstacks. When rendering an active tab in `MeshtasticNavDisplay`, you **must** supply a fresh set of decorators (using `remember(backStack) { ... }`) bound to the active backstack instance to prevent permanent `ViewModelStore` destruction.
|
||||
- **Custom Backstack Mutation:** Do **not** mutate back navigation with custom stacks disconnected from the app backstack. Mutate `NavBackStack<NavKey>` directly with `add(...)` and `removeLastOrNull()`.
|
||||
|
||||
## Reference Anchors
|
||||
- **App Startup / Koin Bootstrap:** `app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt`
|
||||
- **DI Bootstrap Object:** `app/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt`
|
||||
- **DI App Wiring:** `app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt`
|
||||
- **Shared Routes:** `core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt`
|
||||
- **Desktop Nav Shell:** `desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt`
|
||||
79
.skills/new-branch/SKILL.md
Normal file
79
.skills/new-branch/SKILL.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# Skill: New Branch Bootstrap
|
||||
|
||||
## Description
|
||||
Canonical recipe for spinning up a fresh working branch off the latest upstream `main`. Use this skill
|
||||
whenever the user says things like *"make a new branch off fetched origin/main"*, *"peel off a fresh
|
||||
branch"*, *"dust off #NNNN"*, or otherwise signals the start of a new unit of work.
|
||||
|
||||
This replaces the ad-hoc prose that used to be retyped at the start of every session.
|
||||
|
||||
## When to Use
|
||||
- Starting any new feature, fix, chore, or refactor.
|
||||
- Rebasing a stale PR onto current `main` (see [Rebase variant](#rebase-variant)).
|
||||
- Reproducing a CI failure from a clean baseline.
|
||||
|
||||
## Preconditions (verify before branching)
|
||||
1. **Clean worktree.** If `git status --porcelain` is non-empty, ask the user before proceeding.
|
||||
2. **Upstream remote present.** `git remote -v` must list `upstream` pointing at
|
||||
`meshtastic/Meshtastic-Android`. If only `origin` exists on a fork, treat `origin` as upstream.
|
||||
3. **Submodules initialised.** `core/proto/src/main/proto` must be populated — see AGENTS.md
|
||||
workspace bootstrap rules.
|
||||
4. **Secrets bootstrapped.** If `local.properties` is missing, copy `secrets.defaults.properties`
|
||||
(required for `google` flavor builds).
|
||||
|
||||
## Standard Recipe
|
||||
|
||||
```bash
|
||||
# 1. Fetch latest upstream
|
||||
git fetch upstream --prune --tags
|
||||
|
||||
# 2. Create the branch from upstream/main (never from a local stale main)
|
||||
git switch -c <branch-name> upstream/main
|
||||
|
||||
# 3. Ensure submodules track the new base
|
||||
git submodule update --init --recursive
|
||||
|
||||
# 4. Sanity check
|
||||
git --no-pager log -1 --oneline
|
||||
```
|
||||
|
||||
## Branch Naming
|
||||
Use conventional-commit style prefixes that match the PR title convention in AGENTS.md
|
||||
`<git_and_prs>`:
|
||||
|
||||
| Prefix | Use for |
|
||||
| :--- | :--- |
|
||||
| `feat/<scope>` | New user-visible behavior |
|
||||
| `fix/<scope>` | Bug fixes |
|
||||
| `refactor/<scope>` | Code structure changes, no behavior change |
|
||||
| `chore/<scope>` | Tooling, deps, CI, cleanup |
|
||||
| `docs/<scope>` | Documentation only |
|
||||
|
||||
Keep the slug short and kebab-case, e.g. `fix/r8-animation-release`, `chore/koin-application-migration`.
|
||||
|
||||
## Rebase Variant
|
||||
When the user says *"rebase #NNNN"* or *"dust off PR NNNN"*:
|
||||
|
||||
```bash
|
||||
git fetch upstream --prune
|
||||
gh pr checkout <NNNN> # checks out the PR head locally
|
||||
git rebase upstream/main
|
||||
git submodule update --init --recursive
|
||||
# Resolve conflicts, then:
|
||||
git push --force-with-lease
|
||||
```
|
||||
|
||||
Never use plain `--force`. Always `--force-with-lease` to avoid clobbering collaborator pushes.
|
||||
|
||||
## Post-Branch Checklist
|
||||
- [ ] Branch name follows conventional prefix.
|
||||
- [ ] Submodules up to date.
|
||||
- [ ] `local.properties` exists.
|
||||
- [ ] `ANDROID_HOME` exported (see AGENTS.md workspace bootstrap).
|
||||
- [ ] Optional: run `./gradlew assembleDebug` once to catch environment regressions before editing.
|
||||
|
||||
## Tip: Prefer `/delegate` for Long Audits
|
||||
If the user's opening prompt is a sweeping audit or investigation (e.g. *"audit changes since
|
||||
v2.7.13 for regressions"*, *"investigate why animations are broken on release"*), consider
|
||||
suggesting `/delegate` — the GitHub cloud agent can execute the branch + investigation + PR
|
||||
end-to-end while the user keeps working locally. See AGENTS.md `<copilot_cli_workflow>`.
|
||||
83
.skills/project-overview/SKILL.md
Normal file
83
.skills/project-overview/SKILL.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Skill: Project Overview & Codebase Map
|
||||
|
||||
## Description
|
||||
Module directory, namespacing conventions, environment setup, and troubleshooting for Meshtastic-Android.
|
||||
|
||||
- **Build System:** Gradle (Kotlin DSL). JDK 21 REQUIRED. Target SDK: API 36. Min SDK: API 26.
|
||||
- **Flavors:** `fdroid` (OSS only) · `google` (Maps + DataDog analytics)
|
||||
- **Android-only Modules:** `core:api` (AIDL), `core:barcode` (CameraX). Shared contracts abstracted into `core:ui/commonMain`.
|
||||
|
||||
## Codebase Map
|
||||
|
||||
| Directory | Description |
|
||||
| :--- | :--- |
|
||||
| `app/` | Main application module. Contains `MainActivity`, Koin DI modules, and app-level logic. Uses package `org.meshtastic.app`. |
|
||||
| `build-logic/` | Convention plugins for shared build configuration (e.g., `meshtastic.kmp.feature`, `meshtastic.kmp.library`, `meshtastic.kmp.jvm.android`, `meshtastic.koin`). |
|
||||
| `config/` | Detekt static analysis rules (`config/detekt/detekt.yml`) and Spotless formatting config (`config/spotless/.editorconfig`). |
|
||||
| `docs/` | Architecture docs and agent playbooks. See `docs/kmp-status.md` and `docs/roadmap.md` for current status. |
|
||||
| `core/model` | Domain models and common data structures. |
|
||||
| `core:proto` | Protobuf definitions (Git submodule). |
|
||||
| `core:common` | Low-level utilities, I/O abstractions (Okio), and common types. |
|
||||
| `core:database` | Room KMP database implementation. |
|
||||
| `core:datastore` | Multiplatform DataStore for preferences. |
|
||||
| `core:repository` | High-level domain interfaces (e.g., `NodeRepository`, `LocationRepository`). |
|
||||
| `core:domain` | Pure KMP business logic and UseCases. |
|
||||
| `core:data` | Core manager implementations and data orchestration. |
|
||||
| `core:network` | KMP networking layer using Ktor, MQTT abstractions, and shared transport (`StreamFrameCodec`, `TcpTransport`, `SerialTransport`, `BleRadioInterface`). |
|
||||
| `core:di` | Common DI qualifiers and dispatchers. |
|
||||
| `core:navigation` | Shared navigation keys/routes for Navigation 3 using `@Serializable sealed interface` hierarchies. `DeepLinkRouter` for typed backstack synthesis, and `MeshtasticNavSavedStateConfig` with `subclassesOfSealed()` for automatic polymorphic backstack persistence. |
|
||||
| `core:ui` | Shared Compose UI components (`MeshtasticAppShell`, `MeshtasticNavDisplay`, `MeshtasticNavigationSuite`, `AlertHost`, `SharedDialogs`, `PlaceholderScreen`, `MainAppBar`, dialogs, preferences) and platform abstractions. |
|
||||
| `core:service` | KMP service layer; Android bindings stay in `androidMain`. |
|
||||
| `core:api` | Public AIDL/API integration module for external clients. |
|
||||
| `core:prefs` | KMP preferences layer built on DataStore abstractions. |
|
||||
| `core:barcode` | Barcode scanning (Android-only). |
|
||||
| `core:nfc` | NFC abstractions (KMP). Android NFC hardware implementation in `androidMain`. |
|
||||
| `core/ble/` | Bluetooth Low Energy stack using Kable. |
|
||||
| `core/resources/` | Centralized string and image resources (Compose Multiplatform). |
|
||||
| `core/testing/` | Shared test doubles, fakes, and utilities for `commonTest` across all KMP modules. |
|
||||
| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`, `firmware`, `wifi-provision`, `widget`). All are KMP except `widget`. Use `meshtastic.kmp.feature` convention plugin. |
|
||||
| `feature/wifi-provision` | KMP WiFi provisioning via BLE (Nymea protocol). Uses `core:ble` Kable abstractions. |
|
||||
| `feature/firmware` | Fully KMP firmware update system: Unified OTA (BLE + WiFi), native Nordic Secure DFU protocol (pure KMP), USB/UF2 updates, and `FirmwareRetriever` with manifest-based resolution. Desktop is a first-class target. |
|
||||
| `desktop/` | Compose Desktop application. Thin host shell relying on feature modules for shared UI. Full Koin DI graph, TCP, Serial/USB, and BLE transports. Versioning via `config.properties` + `GitVersionValueSource`. |
|
||||
|
||||
## Namespacing
|
||||
- **Standard:** Use the `org.meshtastic.*` namespace for all code.
|
||||
- **Legacy:** Maintain the `com.geeksville.mesh` Application ID.
|
||||
|
||||
## Environment Setup
|
||||
1. **JDK 21 MUST be used** to prevent Gradle sync/build failures.
|
||||
2. **Secrets:** Copy `secrets.defaults.properties` to `local.properties`:
|
||||
```properties
|
||||
MAPS_API_KEY=dummy_key
|
||||
datadogApplicationId=dummy_id
|
||||
datadogClientToken=dummy_token
|
||||
```
|
||||
|
||||
## Workspace Bootstrap (MUST run before any build)
|
||||
Agents **MUST** perform these steps automatically at the start of every session before running any Gradle task. Do not wait for the user to tell you.
|
||||
|
||||
1. **Android SDK:** `ANDROID_HOME` may not be set in agent workspaces. Detect and export it:
|
||||
```bash
|
||||
# Check common macOS/Linux locations in order of preference
|
||||
if [ -z "$ANDROID_HOME" ]; then
|
||||
for dir in "$HOME/Library/Android/sdk" "$HOME/Android/Sdk" "/opt/android-sdk"; do
|
||||
if [ -d "$dir" ]; then export ANDROID_HOME="$dir"; break; fi
|
||||
done
|
||||
fi
|
||||
```
|
||||
All `./gradlew` invocations must include `ANDROID_HOME` in the environment. If the SDK cannot be found, ask the user for the path.
|
||||
|
||||
2. **Proto submodule:** `core/proto/src/main/proto` is a Git submodule containing Protobuf definitions. It must be initialized or builds will fail with proto generation errors:
|
||||
```bash
|
||||
git submodule update --init
|
||||
```
|
||||
|
||||
3. **Init secrets:** If `local.properties` does not exist, copy `secrets.defaults.properties` to `local.properties`. Without this the `google` flavor build fails:
|
||||
```bash
|
||||
[ -f local.properties ] || cp secrets.defaults.properties local.properties
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
- **Build Failures:** Check `gradle/libs.versions.toml` for dependency conflicts.
|
||||
- **Configuration Cache:** Add `--no-configuration-cache` if cache-related issues persist.
|
||||
- **Koin Injection Failures:** Verify the component is included in `AppKoinModule`.
|
||||
85
.skills/testing-ci/SKILL.md
Normal file
85
.skills/testing-ci/SKILL.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Skill: Testing and CI Verification
|
||||
|
||||
## Description
|
||||
Guidelines and commands for verifying code changes locally and understanding the Meshtastic-Android CI pipeline. Use this to determine which testing matrix is needed based on the change type.
|
||||
|
||||
## 1) Baseline local verification order
|
||||
|
||||
Run in a single invocation for routine changes to ensure code formatting, analysis, and basic compilation:
|
||||
|
||||
```bash
|
||||
./gradlew spotlessCheck spotlessApply detekt assembleDebug test allTests
|
||||
```
|
||||
|
||||
> **Why no `clean`?** Incremental builds are safe and significantly faster. Only use `clean` when debugging stale cache issues.
|
||||
|
||||
> **Why `test allTests` and not just `test`:**
|
||||
> In KMP modules, the `test` task name is **ambiguous**. Gradle matches both `testAndroid` and
|
||||
> `testAndroidHostTest` and refuses to run either, silently skipping KMP modules.
|
||||
> `allTests` is the `KotlinTestReport` lifecycle task registered by the KMP plugin.
|
||||
> Conversely, `allTests` does **not** cover pure-Android modules (`:app`, `:core:api`, etc.), which is why both `test` and `allTests` are needed.
|
||||
|
||||
*Note: If testing Compose UI on the JVM (Robolectric) with Java 21, pin tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility crashes.*
|
||||
|
||||
## 2) Change-type verification matrix
|
||||
|
||||
- `docs-only` changes: Usually no Gradle run required, but run `spotlessCheck` if practical.
|
||||
- `UI text/resource` changes: `spotlessCheck`, `detekt`, `assembleDebug`.
|
||||
- `feature/commonMain logic` changes: `spotlessCheck`, `detekt`, `test allTests`, `assembleDebug`.
|
||||
- `navigation/DI wiring` changes: `spotlessCheck`, `detekt`, `assembleDebug`, `test allTests`, plus flavor unit tests if available.
|
||||
- If touching any KMP module, also run `kmpSmokeCompile`.
|
||||
- `worker/service/background` changes: Broad tests, targeted WorkManager checks.
|
||||
- `BLE/networking/core repository`: `spotlessCheck`, `detekt`, `assembleDebug`, `test allTests`.
|
||||
|
||||
## 3) Flavor checks
|
||||
|
||||
Run these when relevant to map, provider, or flavor-specific behavior:
|
||||
|
||||
```bash
|
||||
./gradlew lintFdroidDebug lintGoogleDebug
|
||||
./gradlew testFdroidDebug testGoogleDebug
|
||||
```
|
||||
|
||||
## 4) CI Pipeline Architecture
|
||||
|
||||
CI is defined in `.github/workflows/reusable-check.yml` and structured as four parallel job groups:
|
||||
|
||||
1. **`lint-check`** — Runs spotless, detekt, Android lint, and KMP smoke compile in a single Gradle invocation (avoids 3x cold-start overhead). Uses `fetch-depth: 0` (full clone) for spotless ratcheting and version code calculation. Produces `cache_read_only` output and computed `version_code` for downstream jobs.
|
||||
2. **`test-shards`** — A 3-shard matrix that runs unit tests in parallel (depends on `lint-check`):
|
||||
- `shard-core`: `allTests` for all `core:*` KMP modules.
|
||||
- `shard-feature`: `allTests` for all `feature:*` KMP modules.
|
||||
- `shard-app`: Explicit test tasks for pure-Android/JVM modules (`app`, `desktop`, `core:barcode`).
|
||||
Each shard generates Kover XML coverage and uploads test results + coverage to Codecov with per-shard flags.
|
||||
Downstream jobs use `fetch-depth: 1` and receive `VERSION_CODE` from lint-check via env var, enabling shallow clones.
|
||||
3. **`android-check`** — Builds APKs for all flavors (depends on `lint-check`).
|
||||
4. **`build-desktop`** — Multi-OS matrix (`macos-latest`, `windows-latest`, `ubuntu-24.04`, `ubuntu-24.04-arm`) that builds desktop distributions via `createDistributable` (depends on `lint-check`).
|
||||
|
||||
### Runner Strategy (Three Tiers)
|
||||
- **`ubuntu-24.04-arm`** — Lightweight/utility jobs (status checks, labelers, triage, changelog, release metadata, stale, moderation). Benefits from ARM runners' shorter queue times.
|
||||
- **`ubuntu-24.04`** — Main Gradle-heavy jobs (CI `lint-check`/`test-shards`/`android-check`, release builds, Dokka, publish, dependency-submission). Pin for reproducibility.
|
||||
- **Desktop runners:** Multi-OS matrix (`macos-latest`, `windows-latest`, `ubuntu-24.04`, `ubuntu-24.04-arm`) for the `build-desktop` job and release packaging.
|
||||
|
||||
### CI Gradle Properties
|
||||
`gradle.properties` is tuned for local dev (8g heap, 4g Kotlin daemon). CI uses `.github/ci-gradle.properties`, which the `gradle-setup` composite action copies to `~/.gradle/gradle.properties`. Key CI overrides:
|
||||
- `org.gradle.daemon=false` (single-use runners)
|
||||
- `kotlin.incremental=false` (fresh checkouts)
|
||||
- `-Xmx4g` Gradle heap, `-Xmx2g` Kotlin daemon
|
||||
- VFS watching disabled, workers capped at 4
|
||||
- `org.gradle.isolated-projects=true` for better parallelism
|
||||
- Disables unused Android build features (`resvalues`, `shaders`)
|
||||
|
||||
### CI Conventions
|
||||
- **KMP Smoke Compile:** `./gradlew kmpSmokeCompile` is a lifecycle task (registered in `RootConventionPlugin`) that auto-discovers all KMP modules and depends on their `compileKotlinJvm` + `compileKotlinIosSimulatorArm64` tasks.
|
||||
- **`maxParallelForks` CI logic:** `ProjectExtensions.kt` checks `project.findProperty("ci") == "true"` and uses full available processors in CI (4 forks on std runners) vs. half locally. All CI invocations pass `-Pci=true`.
|
||||
- **Detekt report formats:** Detekt.kt checks `project.findProperty("ci") == "true"` and disables html, txt, md reports in CI; only xml + sarif are retained for GitHub annotations.
|
||||
- **Robolectric SDK caching:** The `gradle-setup` composite action caches `~/.m2/repository/org/robolectric` to prevent flaky `SocketException` on SDK downloads. Cache key is `robolectric-{version}-sdk{level}` — update when bumping version or SDK level.
|
||||
- **`mavenLocal()` gated:** Disabled by default to prevent CI cache poisoning. Pass `-PuseMavenLocal` for local JitPack testing.
|
||||
- **JUnit parallel execution:** Enabled project-wide with classes running sequentially (`junit.jupiter.execution.parallel.mode.classes.default=same_thread`) to avoid `Dispatchers.setMain()` races. Cross-module parallelism comes from Gradle forks (`maxParallelForks`).
|
||||
- **`test-retry` plugin:** Applied to all module types (maxRetries=2, maxFailures=10).
|
||||
- **`fail-fast: false`:** Test sharding does not cancel other shards on failure.
|
||||
- **Explicit Gradle task paths:** Prefer `app:lintFdroidDebug` over shorthand `lintDebug` in CI.
|
||||
- **Pull request CI:** Main-only (`.github/workflows/pull-request.yml` targets `main`).
|
||||
- **Cache writes:** Trusted on `main` and merge queue runs; other refs use read-only cache.
|
||||
- **Path filtering:** `check-changes` in `pull-request.yml` must include module dirs plus build/workflow entrypoints (`build-logic/**`, `gradle/**`, `.github/workflows/**`, `gradlew`, `settings.gradle.kts`, etc.).
|
||||
- **AboutLibraries:** Runs in `offlineMode` by default (no GitHub/SPDX API calls). Release builds pass `-PaboutLibraries.release=true` via Fastlane/Gradle CLI to enable remote license fetching. Do NOT re-gate on `CI` or `GITHUB_TOKEN` alone.
|
||||
|
||||
228
AGENTS.md
228
AGENTS.md
|
|
@ -1,140 +1,108 @@
|
|||
# Meshtastic Android - Agent Guide
|
||||
# Meshtastic Android - Unified Agent & Developer Guide
|
||||
|
||||
This file serves as a comprehensive guide for AI agents and developers working on the `Meshtastic-Android` codebase. Use this as your primary reference for understanding the architecture, conventions, and workflows.
|
||||
<role>
|
||||
You are an expert Android and Kotlin Multiplatform (KMP) engineer working on Meshtastic-Android, a decentralized mesh networking application. You must maintain strict architectural boundaries, use Modern Android Development (MAD) standards, and adhere to Compose Multiplatform and JetBrains Navigation 3 patterns.
|
||||
</role>
|
||||
|
||||
## 1. Project Overview
|
||||
<context_and_memory>
|
||||
- **Project Goal:** Decouple business logic from the Android framework for seamless multi-platform execution (Android, Desktop, iOS) while maintaining a high-performance native Android experience.
|
||||
- **Language & Tech:** Kotlin 2.3+ (JDK 21 REQUIRED), Gradle Kotlin DSL, Ktor, Okio, Room KMP.
|
||||
- **Core Architecture:**
|
||||
- `commonMain` is pure KMP. `androidMain` is strictly for Android framework bindings.
|
||||
- App root DI and graph assembly live in the `app` and `desktop` host shells.
|
||||
- **Skills Directory:** You **MUST** consult the relevant `.skills/` module before executing work:
|
||||
- `.skills/project-overview/` - Codebase map, module directory, namespacing, environment setup, troubleshooting.
|
||||
- `.skills/kmp-architecture/` - Bridging, expect/actual, source-sets, catalog aliases, build-logic conventions.
|
||||
- `.skills/compose-ui/` - Adaptive UI, placeholders, string resources.
|
||||
- `.skills/navigation-and-di/` - JetBrains Navigation 3 & Koin 4.2+ annotations.
|
||||
- `.skills/testing-ci/` - Validation commands, CI pipeline architecture, CI Gradle properties.
|
||||
- `.skills/implement-feature/` - Step-by-step feature workflow.
|
||||
- `.skills/code-review/` - PR validation checklist.
|
||||
- `.skills/new-branch/` - Canonical recipe for branching off upstream/main and rebasing stale PRs.
|
||||
- **Active Status:** Read `docs/kmp-status.md` and `docs/roadmap.md` to understand the current KMP migration epoch.
|
||||
</context_and_memory>
|
||||
|
||||
- **Type:** Native Android Application (Kotlin).
|
||||
- **Purpose:** Client interface for Meshtastic mesh radios.
|
||||
- **Architecture:** Modern Android Development (MAD) principles.
|
||||
- **UI:** Jetpack Compose (Material 3).
|
||||
- **State Management:** Unidirectional Data Flow (UDF) with ViewModels, Coroutines, and Flow.
|
||||
- **Dependency Injection:** Hilt.
|
||||
- **Navigation:** Type-Safe Navigation (Jetpack Navigation).
|
||||
- **Data Layer:** Repository pattern with Room (local DB), DataStore (prefs), and Protobuf (device comms).
|
||||
<process>
|
||||
- **Workspace Bootstrap (MUST run first):** Before executing any Gradle task in a new workspace, agents MUST automatically:
|
||||
1. **Find the Android SDK** — `ANDROID_HOME` is often unset in agent worktrees. Probe `~/Library/Android/sdk`, `~/Android/Sdk`, and `/opt/android-sdk`. Export the first one found. If none exist, ask the user.
|
||||
2. **Init the proto submodule** — Run `git submodule update --init`. The `core/proto/src/main/proto` submodule contains Protobuf definitions required for builds.
|
||||
3. **Init secrets** — If `local.properties` does not exist, copy `secrets.defaults.properties` to `local.properties`. Without this the `google` flavor build fails.
|
||||
- **Think First:** Reason through the problem before writing code. For complex KMP tasks involving multiple modules or source sets, outline your approach step-by-step before executing.
|
||||
- **Plan Before Execution:** Use the git-ignored `.agent_plans/` directory to write markdown implementation plans (`plan.md`) and Mermaid diagrams (`.mmd`) for complex refactors before modifying code.
|
||||
- **Atomic Execution:** Follow your plan step-by-step. Do not jump ahead. Use TDD where feasible (write `commonTest` fakes first).
|
||||
- **Baseline Verification:** Always instruct the user (or use your CLI tools) to run the baseline check before finishing:
|
||||
```
|
||||
./gradlew spotlessCheck spotlessApply detekt assembleDebug test allTests
|
||||
```
|
||||
> **Why both `test` and `allTests`?** In KMP modules, `test` is ambiguous and Gradle silently skips them. `allTests` is the KMP lifecycle task that covers KMP modules. Conversely, `allTests` does NOT cover pure-Android modules (`:app`, `:core:api`), so both tasks are required.
|
||||
> For KMP cross-platform compilation, also run `./gradlew kmpSmokeCompile` (compiles all KMP modules for JVM + iOS Simulator — used by CI's `lint-check` job).
|
||||
</process>
|
||||
|
||||
## 2. Codebase Map
|
||||
<agent_tools>
|
||||
- **Codebase Search:** Use whatever search and navigation tools your environment provides (file search, grep/ripgrep, symbol lookup, semantic search, etc.) to map out project boundaries before coding. Prefer `rg` (ripgrep) over `grep` or `find` for raw text search.
|
||||
- **Terminal Pagers:** When running shell commands like `git diff` or `git log`, ALWAYS use `--no-pager` (e.g., `git --no-pager diff`) to prevent getting stuck in an interactive prompt.
|
||||
- **Fetch Up-to-Date Docs:** If your environment supports web search, MCP servers, or documentation lookup tools, actively query them for the latest documentation on Koin 4.x, JetBrains Navigation 3, and Compose Multiplatform 1.11.
|
||||
- **Clone Reference Repos:** If documentation is insufficient, use shell commands to clone bleeding-edge KMP dependency repositories into the local `.agent_refs/` directory (git-ignored) to inspect their source and test suites. Recommended:
|
||||
- `https://github.com/JetBrains/kotlin-multiplatform-dev-docs` (Official Docs)
|
||||
- `https://github.com/InsertKoinIO/koin` (Koin Annotations 4.x)
|
||||
- `https://github.com/JetBrains/compose-multiplatform` (Navigation 3, Adaptive UI)
|
||||
- `https://github.com/JuulLabs/kable` (BLE)
|
||||
- `https://github.com/coil-kt/coil` (Coil 3 KMP)
|
||||
- `https://github.com/ktorio/ktor` (Ktor Networking)
|
||||
- **Formatting Hooks:** Always run `./gradlew spotlessApply` as an automatic formatting hook to fix style violations after editing.
|
||||
</agent_tools>
|
||||
|
||||
| Directory | Description |
|
||||
| :--- | :--- |
|
||||
| `app/` | Main application module. Contains `MainActivity`, `AppNavigation`, and Hilt entry points. Uses package `com.geeksville.mesh`. |
|
||||
| `core/` | Shared library modules. Most code here uses package `org.meshtastic.core.*`. |
|
||||
| `core/ble/` | **New:** Bluetooth Low Energy stack using Nordic libraries. |
|
||||
| `core/strings/` | **Crucial:** Centralized string resources using Compose Multiplatform Resources. |
|
||||
| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`). Each is a standalone Gradle module. Uses package `org.meshtastic.feature.*`. |
|
||||
| `build-logic/` | Custom Gradle convention plugins. Defines build logic for the entire project. |
|
||||
| `gradle/libs.versions.toml` | **Version Catalog.** All dependencies and versions are defined here. |
|
||||
| `core/proto/` | Protobuf definitions for communicating with the mesh radio. |
|
||||
<documentation_sync>
|
||||
`AGENTS.md` is the single source of truth for agent instructions. Agent-specific files redirect here:
|
||||
- `.github/copilot-instructions.md` — Copilot redirect to `AGENTS.md`.
|
||||
- `CLAUDE.md` — Claude Code entry point; imports `AGENTS.md` via `@AGENTS.md` and adds Claude-specific instructions.
|
||||
- `GEMINI.md` — Gemini redirect to `AGENTS.md`. Gemini CLI also configured via `.gemini/settings.json` to read `AGENTS.md` directly.
|
||||
|
||||
## 3. Development Guidelines
|
||||
Do NOT duplicate content into agent-specific files. When you modify architecture, module targets, CI tasks, validation commands, or agent workflow rules, update `AGENTS.md`, `.skills/`, and `docs/kmp-status.md` as needed.
|
||||
</documentation_sync>
|
||||
|
||||
### A. UI Development (Jetpack Compose)
|
||||
- **Material 3:** The app uses Material 3. Look for ways to use **Material 3 Expressive** components where appropriate.
|
||||
- **Strings:**
|
||||
- Do **not** use `app/src/main/res/values/strings.xml` for UI strings.
|
||||
- Use the **Compose Multiplatform Resource** library in `core:resources`.
|
||||
- **Definition:** Add strings to `core/resources/src/commonMain/composeResources/values/strings.xml`.
|
||||
- **Usage:**
|
||||
```kotlin
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.your_string_key
|
||||
<rules>
|
||||
- **No Lazy Coding:** DO NOT use placeholders like `// ... existing code ...`. Always provide complete, valid code blocks for the sections you modify to ensure correct diff application.
|
||||
- **No Framework Bleed:** NEVER import `java.*` or `android.*` in `commonMain`. Use KMP equivalents: `Okio` for `java.io.*`, `kotlinx.coroutines.sync.Mutex` for `java.util.concurrent.locks.*`, `atomicfu` or Mutex-guarded `mutableMapOf()` for `ConcurrentHashMap`. Use `org.meshtastic.core.common.util.ioDispatcher` instead of `Dispatchers.IO` directly.
|
||||
- **Koin Annotations:** Use `@Single`, `@Factory`, and `@KoinViewModel` inside `commonMain` instead of manual constructor trees. Do not enable A1 module compile safety — A3 full-graph validation (`VerifyModule`) is the correct approach because interfaces and implementations live in separate modules. Always register new feature modules in **both** `AppKoinModule.kt` and `DesktopKoinModule.kt`; they are not auto-activated.
|
||||
- **CMP Over Android:** Use `compose-multiplatform` constraints. `stringResource` only supports `%N$s` and `%N$d` — pre-format floats with `NumberFormatter.format()` from `core:common` and pass as `%N$s`. In ViewModels/coroutines use `getStringSuspend(Res.string.key)`; never blocking `getString()`. Always use `MeshtasticNavDisplay` (not raw `NavDisplay`) as the navigation host, and `NavigationBackHandler` (not Android's `BackHandler`) for back gestures in shared code.
|
||||
- **ProGuard:** When adding a reflection-heavy dependency, add keep rules to **both** `app/proguard-rules.pro` and `desktop/proguard-rules.pro` and verify release builds.
|
||||
- **Always Check Docs:** If unsure about an abstraction, search `core:ui/commonMain` or `core:navigation/commonMain` before assuming it doesn't exist.
|
||||
- **Privacy First:** Never log or expose PII, location data, or cryptographic keys. Meshtastic is used for sensitive off-grid communication — treat all user data with extreme caution.
|
||||
- **Dependency Discipline:** Never add a library without first checking `libs.versions.toml` and justifying its inclusion against the project's size and complexity goals. Prefer removing dependencies over adding them.
|
||||
- **Zero Lint Tolerance:** A task is incomplete if `detekt` fails or `spotlessCheck` does not pass for touched modules.
|
||||
- **Read Before Refactoring:** When a pattern contradicts best practices, analyze whether it is legacy debt or a deliberate architectural choice before proposing a change.
|
||||
</rules>
|
||||
|
||||
Text(text = stringResource(Res.string.your_string_key))
|
||||
```
|
||||
- **Dialogs:**
|
||||
- Use the centralized `MeshtasticDialog` for all alerts and confirmation boxes.
|
||||
- **Specialized Overloads:** Use `MeshtasticResourceDialog` (for resource-only content) or `MeshtasticTextDialog` (for mixed resource/text content) to reduce boilerplate.
|
||||
- **Location:** Defined in `core/ui/src/main/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt`.
|
||||
- **Previews:** Create `@Preview` functions for your Composables to ensure they render correctly.
|
||||
<copilot_cli_workflow>
|
||||
These tips apply when the agent is the GitHub Copilot CLI. Other agent runtimes may ignore this
|
||||
section.
|
||||
|
||||
### B. Architecture & State
|
||||
- **ViewModels:** Must be annotated with `@HiltViewModel`.
|
||||
- **Injection:** Use `@Inject constructor(...)`.
|
||||
- **Scopes:** Use `viewModelScope` for coroutines. Avoid `GlobalScope`.
|
||||
- **Data Flow:** Expose UI state as `StateFlow<UiState>` or `Flow<UiState>`.
|
||||
- **Delegate long autonomous work.** For sweeping audits, multi-hour investigations, or "fleet"
|
||||
prompts (*"investigate why X is broken on release"*, *"audit the diff since tag vX.Y.Z"*,
|
||||
*"review the codebase for best practices against spec Z"*), prefer `/delegate` so the GitHub
|
||||
cloud agent opens a PR while the user keeps working locally. Don't tie up an interactive
|
||||
session on work that can run unattended.
|
||||
- **Use `/research` for "latest hotness" prompts.** When the user asks for *"the latest scoop"*
|
||||
on Kotlin / KMP / Compose / Koin trends, the built-in `/research` slash command performs deep
|
||||
research across GitHub and the web with better source grounding than an ad-hoc prompt.
|
||||
- **Use `/plan` mode for "noodle it out" prompts.** When the user asks for an implementation
|
||||
plan, a "walk me through next steps", or explicitly says "don't do anything yet" — switch to
|
||||
plan mode (Shift+Tab or `/plan`). Plans persist in the session workspace and keep the agent
|
||||
from prematurely editing files. Continue to write long-form plans and Mermaid diagrams to
|
||||
`.agent_plans/` (git-ignored) for multi-module refactors.
|
||||
- **`/share` audit and review outputs.** After large audits, PR safety reviews, or release-cycle
|
||||
quality passes, offer `/share` to export the findings to a gist or markdown file. These
|
||||
reports are valuable artifacts — don't let them die in session history.
|
||||
- **Prefer `/rewind` or `ctrl+s` over retyping.** If a turn went sideways, `/rewind` reverts
|
||||
file changes and the turn; `ctrl+s` submits while preserving the input for quick iteration.
|
||||
Avoid re-issuing the same prompt verbatim.
|
||||
- **New-branch flow lives in a skill.** When the user says "fresh branch off fetched origin/main"
|
||||
or "rebase PR #NNNN", consult `.skills/new-branch/SKILL.md` rather than re-deriving the recipe.
|
||||
</copilot_cli_workflow>
|
||||
|
||||
### C. Navigation
|
||||
- The project uses **Type-Safe Navigation** (Kotlin Serialization).
|
||||
- Routes are defined in `core/navigation` (e.g., `ContactsRoutes`, `SettingsRoutes`).
|
||||
- The main `NavHost` is located in `app/src/main/java/com/geeksville/mesh/ui/Main.kt`.
|
||||
|
||||
### D. Bluetooth (BLE)
|
||||
- **Library:** Uses **Nordic Semiconductor's Kotlin BLE Library** and **Android Common Libraries**.
|
||||
- **Location:** Core logic resides in `core/ble`.
|
||||
- **Key Classes:** `BluetoothRepository`, `NordicBleInterface`, `BleConnection`.
|
||||
- **Usage:** Use `BluetoothRepository` for scanning and bonding. Use `BleConnection` for managing connections. Avoid legacy `BluetoothAdapter` APIs directly.
|
||||
- **Environment Mocking:** Use `LocalEnvironmentOwner` and `MockAndroidEnvironment` to test UI hardware reactions without a real device.
|
||||
|
||||
### E. Dependency Management
|
||||
- **Never** hardcode versions in `build.gradle.kts` files.
|
||||
- **Action:** Add the library and version to `gradle/libs.versions.toml`.
|
||||
- **Action:** Apply plugins using the alias from the catalog (e.g., `alias(libs.plugins.meshtastic.android.library)`).
|
||||
- **Alpha Libraries:** Do not be shy about using alpha libraries from Google if they provide necessary features.
|
||||
|
||||
### F. Build Variants (Flavors)
|
||||
- **`google`**: Includes Google Play Services (Maps, Firebase, Crashlytics).
|
||||
- **`fdroid`**: FOSS version. **Strictly segregate sensitive data** (Crashlytics, Firebase, etc.) out of this flavor.
|
||||
- **Task Example:** `./gradlew assembleFdroidDebug`
|
||||
|
||||
### G. Kotlin Multiplatform (KMP) & Decoupling
|
||||
- **Goal:** We are actively moving logic and models from Android-specific modules to KMP modules (`core:common`, `core:model`, `core:proto`) to support future cross-platform expansion.
|
||||
- **Domain Models:** Always place domain models (Data Classes, Enums) in `commonMain` of the respective module.
|
||||
- **Parceling:**
|
||||
- Use the platform-agnostic `CommonParcelable` and `CommonParcelize` from `core:common`.
|
||||
- Avoid direct imports of `android.os.Parcelable` or `kotlinx.parcelize.Parcelize` in `commonMain`.
|
||||
- **Platform Abstractions:** Use `expect`/`actual` for platform-specific logic (e.g., `DateFormatter`, `RandomUtils`, `BuildUtils`).
|
||||
- **AIDL Compatibility:** AIDL parcelable declarations for models moved to `commonMain` should be relocated to `:core:api` to ensure proper export to consumer modules.
|
||||
|
||||
## 4. Quality Assurance
|
||||
|
||||
### A. Code Style (Spotless)
|
||||
- The project uses **Spotless** to enforce formatting.
|
||||
- **Command:** `./gradlew spotlessApply`
|
||||
- **Rule:** You **must** run this before submitting any code.
|
||||
|
||||
### B. Linting (Detekt)
|
||||
- The project uses **Detekt** for static analysis.
|
||||
- **Command:** `./gradlew detekt`
|
||||
- **Rule:** Ensure zero regressions.
|
||||
|
||||
### C. Testing
|
||||
- **Unit Tests:** JUnit 4/5 in `src/test/java`. Run with `./gradlew test`.
|
||||
- **Compose UI Tests (JVM):** Preferred for component testing. Use **Robolectric** in `src/test/java`.
|
||||
- **Important:** Annotate with `@Config(sdk = [34])` if using Java 17 to avoid SDK 35 compatibility issues.
|
||||
- **Best Practice:** Pass mocked ViewModels to Composables instead of using Hilt in Robolectric tests.
|
||||
- **Instrumented Tests:** For full E2E or Hilt integration tests, use `src/androidTest/java`. Run with `./gradlew connectedAndroidTest`.
|
||||
- **Feature Test:** `./gradlew feature:settings:testGoogleDebug`
|
||||
|
||||
## 5. Agent Workflow
|
||||
|
||||
1. **Explore First:** Before making changes, read `gradle/libs.versions.toml` and the relevant `build.gradle.kts` to understand the environment.
|
||||
2. **Plan:** Identify which modules (`core` or `feature`) need modification.
|
||||
3. **Implement:**
|
||||
- If adding a string, modify `core:resources`.
|
||||
- If adding a dependency, modify `libs.versions.toml` first.
|
||||
4. **Verify:**
|
||||
- Run `./gradlew spotlessApply` (Essential!).
|
||||
- Run `./gradlew detekt`.
|
||||
- Run relevant tests (e.g., `./gradlew :feature:settings:testDebugUnitTest`).
|
||||
|
||||
## 6. Important Context
|
||||
|
||||
- **Protobuf:** Communication with the device uses Protobufs. The definitions are in `core/proto`. This is a Git submodule, but the build system handles it.
|
||||
- **Legacy:** Some code in `app/` uses the `com.geeksville.mesh` package. Newer code in `core/` and `feature/` uses `org.meshtastic.*`. Respect the existing package structure of the file you are editing.
|
||||
- **Versioning:** Do not manually edit `versionCode` or `versionName`. These are managed by the build system and CI/CD.
|
||||
- **Database Safety:** When modifying critical database logic (e.g., `NodeInfoDao`), always ensure you have explicit test coverage for security edge cases (like PKC conflicts or key wiping). Refer to `core/database/src/androidTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoTest.kt` for examples.
|
||||
|
||||
## 7. Troubleshooting
|
||||
|
||||
- **Missing Strings:** If `Res.string.xyz` is unresolved, ensure you have imported `org.meshtastic.core.resources.Res` and the specific string property, and that you have run a build to generate the resources.
|
||||
- **Build Errors:** Check `gradle/libs.versions.toml` for version conflicts. Use `build-logic` conventions to ensure plugins are applied correctly.
|
||||
|
||||
---
|
||||
*Refer to `CONTRIBUTING.md` for human-centric processes like Code of Conduct and Pull Request etiquette.*
|
||||
|
||||
### E. Resources and Assets
|
||||
- **Centralization:** All global app resources (Strings, Drawables, Fonts, raw files) should be placed in `:core:resources`.
|
||||
- **Module Path:** `core/resources/src/commonMain/composeResources/`
|
||||
- **Decentralization:** Feature-specific strings and assets can (and should) be housed in their respective feature module's `composeResources` directory to maintain modular boundaries and clean architectural dependency graphs. Crowdin localization handles globbing `/**/composeResources/values/strings.xml` perfectly.
|
||||
- **Drawables:** Use `painterResource(Res.drawable.your_icon)` to access cross-platform drawables. Name them consistently (`ic_` for icons, `img_` for artwork). Avoid putting standard Drawables or Vectors in legacy Android `res/drawable` folders unless strictly required by a legacy library (like `OsmDroid` map markers) or the OS layer (like `app_icon.xml`).
|
||||
<git_and_prs>
|
||||
- **Commit Hygiene:** Squash fixup/polish/review-feedback commits before opening a PR. Each commit should represent a logical, self-contained unit of work — not a back-and-forth conversation.
|
||||
- **PR Descriptions:** Keep PR descriptions concise and scannable. State *what changed* and *why*, not a per-commit play-by-play. Use a short summary paragraph followed by a bullet list of changes. Avoid tables, headers-per-commit, or verbose breakdowns. Reference the `meshtastic/firmware` repo PRs for tone and style.
|
||||
- **PR Titles:** Use conventional commit format: `feat(scope):`, `fix(scope):`, `refactor(scope):`, `chore(scope):`. Keep titles under ~72 characters.
|
||||
</git_and_prs>
|
||||
|
|
|
|||
9
CLAUDE.md
Normal file
9
CLAUDE.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Meshtastic Android - Claude Code Guide
|
||||
|
||||
@AGENTS.md
|
||||
|
||||
## Claude-Specific Instructions
|
||||
|
||||
- **Think First:** Always outline your step-by-step reasoning inside `<thinking>` tags before writing code or shell commands. Claude models perform significantly better on complex KMP tasks when they "think out loud" first.
|
||||
- **Skills:** The `.skills/` directory contains task-specific instruction modules. Load them as needed — only the skill relevant to your current task.
|
||||
- **Plan Mode:** Use plan mode for architectural changes spanning multiple modules. Write plans to `.agent_plans/` (git-ignored). The Copilot-CLI-specific `/plan`, `/delegate`, `/research`, and `/share` guidance in `AGENTS.md` does not apply to Claude Code — skip the `<copilot_cli_workflow>` section.
|
||||
|
|
@ -48,7 +48,7 @@ Meshtastic-Android uses unit tests, Robolectric JVM tests, and instrumented UI t
|
|||
|
||||
- **Unit tests** are located in the `src/test/` directory of each module.
|
||||
- **Compose UI Tests (JVM)** are preferred for component testing and are also located in `src/test/` using **Robolectric**.
|
||||
- Note: If using Java 17, pin your Robolectric tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility issues.
|
||||
- Note: If using Java 21, pin your Robolectric tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility issues.
|
||||
- **Instrumented tests** (including full E2E UI tests) are located in `src/androidTest/`. For Compose UI, use the [Jetpack Compose Testing APIs](https://developer.android.com/jetpack/compose/testing).
|
||||
|
||||
#### Guidelines for Testing
|
||||
|
|
|
|||
6
GEMINI.md
Normal file
6
GEMINI.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Meshtastic Android - Google Gemini Guide
|
||||
|
||||
> **Note:** The canonical instructions for all AI Agents have been deduplicated.
|
||||
|
||||
You MUST immediately read and internalize the unified instructions located at the root of the repository in `AGENTS.md`.
|
||||
After reading `AGENTS.md`, consult the `.skills/` directory for task-specific playbooks.
|
||||
46
Gemfile.lock
46
Gemfile.lock
|
|
@ -3,13 +3,13 @@ GEM
|
|||
specs:
|
||||
CFPropertyList (3.0.8)
|
||||
abbrev (0.1.2)
|
||||
addressable (2.8.8)
|
||||
addressable (2.9.0)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1213.0)
|
||||
aws-sdk-core (3.242.0)
|
||||
aws-partitions (1.1240.0)
|
||||
aws-sdk-core (3.245.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
|
|
@ -17,11 +17,11 @@ GEM
|
|||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.121.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sdk-kms (1.123.0)
|
||||
aws-sdk-core (~> 3, >= 3.244.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.213.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sdk-s3 (1.219.0)
|
||||
aws-sdk-core (~> 3, >= 3.244.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
|
|
@ -29,7 +29,7 @@ GEM
|
|||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
benchmark (0.5.0)
|
||||
bigdecimal (4.0.1)
|
||||
bigdecimal (4.1.2)
|
||||
claide (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
|
|
@ -68,11 +68,11 @@ GEM
|
|||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday-retry (1.0.4)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.4.0)
|
||||
fastlane (2.232.2)
|
||||
fastimage (2.4.1)
|
||||
fastlane (2.233.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
abbrev (~> 0.1.2)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
|
|
@ -92,7 +92,7 @@ GEM
|
|||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 1.0)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
fastlane-sirp (>= 1.0.0)
|
||||
fastlane-sirp (>= 1.1.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
|
|
@ -122,10 +122,9 @@ GEM
|
|||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.4.1)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
fastlane-sirp (1.0.0)
|
||||
sysrandom (~> 1.0)
|
||||
fastlane-sirp (1.1.0)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.95.0)
|
||||
google-apis-androidpublisher_v3 (0.99.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-core (0.18.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
|
|
@ -139,15 +138,15 @@ GEM
|
|||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.17.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-storage_v1 (0.59.0)
|
||||
google-apis-storage_v1 (0.61.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-cloud-core (1.8.0)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (2.1.1)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-errors (1.5.0)
|
||||
google-cloud-storage (1.58.0)
|
||||
google-cloud-errors (1.6.0)
|
||||
google-cloud-storage (1.59.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-core (>= 0.18, < 2)
|
||||
|
|
@ -169,13 +168,13 @@ GEM
|
|||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
json (2.18.1)
|
||||
json (2.19.4)
|
||||
jwt (2.10.2)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.19.1)
|
||||
multi_json (1.20.1)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.4.0)
|
||||
|
|
@ -185,13 +184,13 @@ GEM
|
|||
os (1.1.4)
|
||||
ostruct (0.6.3)
|
||||
plist (3.7.2)
|
||||
public_suffix (7.0.2)
|
||||
rake (13.3.1)
|
||||
public_suffix (7.0.5)
|
||||
rake (13.4.2)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
retriable (3.4.1)
|
||||
rexml (3.4.4)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
|
|
@ -205,7 +204,6 @@ GEM
|
|||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
naturally
|
||||
sysrandom (1.0.5)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
|
|
|
|||
21
README.md
21
README.md
|
|
@ -11,7 +11,7 @@
|
|||
[](https://opencollective.com/meshtastic/)
|
||||
[](https://vercel.com?utm_source=meshtastic&utm_campaign=oss)
|
||||
|
||||
This is a tool for using Android with open-source mesh radios. For more information see our webpage: [meshtastic.org](https://www.meshtastic.org). If you are looking for the device side code, see [here](https://github.com/meshtastic/firmware).
|
||||
This is a tool for using Android (and Compose Desktop) with open-source mesh radios. For more information see our webpage: [meshtastic.org](https://www.meshtastic.org). If you are looking for the device side code, see [here](https://github.com/meshtastic/firmware).
|
||||
|
||||
This project is currently beta testing across various providers. If you have questions or feedback please [Join our discussion forum](https://github.com/orgs/meshtastic/discussions) or the [Discord Group](https://discord.gg/meshtastic) . We would love to hear from you!
|
||||
|
||||
|
|
@ -51,23 +51,24 @@ You can generate the documentation locally to preview your changes.
|
|||
|
||||
1. **Run the Dokka task:**
|
||||
```bash
|
||||
./gradlew :app:dokkaHtml
|
||||
./gradlew dokkaGeneratePublicationHtml
|
||||
```
|
||||
2. **View the output:**
|
||||
The generated HTML files will be located in the `app/build/dokka/html` directory. You can open the `index.html` file in your browser to view the documentation.
|
||||
The generated HTML files will be located in the `build/dokka/html` directory. You can open the `index.html` file in your browser to view the documentation.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Modern Android Development (MAD)
|
||||
The app follows modern Android development practices:
|
||||
- **UI:** Jetpack Compose (Material 3).
|
||||
The app follows modern Android development practices, built on top of a shared Kotlin Multiplatform (KMP) Core:
|
||||
- **KMP Modules:** Business logic (`core:domain`), data sources (`core:data`, `core:database`, `core:datastore`), and communications (`core:network`, `core:ble`) are entirely platform-agnostic, targeting Android and Compose Desktop.
|
||||
- **UI:** JetBrains Compose Multiplatform (Material 3) using Compose Multiplatform resources.
|
||||
- **State Management:** Unidirectional Data Flow (UDF) with ViewModels, Coroutines, and Flow.
|
||||
- **Dependency Injection:** Hilt.
|
||||
- **Navigation:** Type-Safe Navigation (Jetpack Navigation).
|
||||
- **Data Layer:** Repository pattern with Room (local DB), DataStore (prefs), and Protobuf (device comms).
|
||||
- **Dependency Injection:** Koin with Koin Annotations (K2 Compiler Plugin).
|
||||
- **Navigation:** JetBrains Navigation 3 (Multiplatform routing with RESTful deep linking).
|
||||
- **Data Layer:** Repository pattern with Room KMP (local DB), DataStore (prefs), and Protobuf (device comms).
|
||||
|
||||
### Bluetooth Low Energy (BLE)
|
||||
The BLE stack has been modernized to use **Nordic Semiconductor's Android Common Libraries** and **Kotlin BLE Library**. This provides a robust, Coroutine-based architecture for reliable device communication. See [core/ble/README.md](core/ble/README.md) for details.
|
||||
The BLE stack uses a multiplatform interface-driven architecture. Platform-agnostic interfaces live in `commonMain`, utilizing the **Kable** multiplatform BLE library to handle device communication across all supported targets (Android, Desktop). This provides a robust, Coroutine-based architecture for reliable device communication while remaining fully KMP compatible. See [core/ble/README.md](core/ble/README.md) for details.
|
||||
|
||||
## Translations
|
||||
|
||||
|
|
@ -79,6 +80,8 @@ Developers can integrate with the Meshtastic Android app using our published API
|
|||
|
||||
For detailed integration instructions, see [core/api/README.md](core/api/README.md).
|
||||
|
||||
Additionally, the app includes a built-in **Local TAK Server** feature that can be enabled in settings. This runs a local TCP server on port 8089 to allow ATAK clients to connect directly and route their traffic over the mesh.
|
||||
|
||||
## Building the Android App
|
||||
> [!WARNING]
|
||||
> Debug and release builds can be installed concurrently. This is solely to enable smoother development, and you should avoid running both apps simultaneously. To ensure proper function, force quit the app not in use.
|
||||
|
|
|
|||
12
SECURITY.md
Normal file
12
SECURITY.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| App Version | Supported |
|
||||
| ---------------- | ------------------ |
|
||||
| 2.7.x | :white_check_mark: |
|
||||
| <= 2.6.x | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We support the private reporting of potential security vulnerabilities. Please go to the Security tab to file a report with a description of the potential vulnerability and reproduction scripts (preferred) or steps, and our developers will review.
|
||||
|
|
@ -6,13 +6,13 @@ The `:app` module is the entry point for the Meshtastic Android application. It
|
|||
## Key Components
|
||||
|
||||
### 1. `MainActivity` & `Main.kt`
|
||||
The single Activity of the application. It hosts the `NavHost` and manages the root UI structure (Navigation Bar, Rail, etc.).
|
||||
The single Activity of the application. It hosts the shared `MeshtasticNavDisplay` navigation shell and manages the root UI structure (Navigation Bar, Rail, etc.).
|
||||
|
||||
### 2. `MeshService`
|
||||
The core background service that manages long-running communication with the mesh radio. It runs as a **Foreground Service** to ensure reliable communication even when the app is in the background.
|
||||
The core background service that manages long-running communication with the mesh radio. While it is declared in the `:app` manifest for system visibility, its implementation resides in the `:core:service` module. It runs as a **Foreground Service** to ensure reliable communication even when the app is in the background.
|
||||
|
||||
### 3. Hilt Application
|
||||
`MeshUtilApplication` is the Hilt entry point, providing the global dependency injection container.
|
||||
### 3. Koin Application
|
||||
`MeshUtilApplication` is the Koin entry point, providing the global dependency injection container.
|
||||
|
||||
## Architecture
|
||||
The module primarily serves as a "glue" layer, connecting:
|
||||
|
|
@ -25,13 +25,13 @@ The module primarily serves as a "glue" layer, connecting:
|
|||
```mermaid
|
||||
graph TB
|
||||
:app[app]:::android-application
|
||||
:app -.-> :core:analytics
|
||||
:app -.-> :core:ble
|
||||
:app -.-> :core:common
|
||||
:app -.-> :core:data
|
||||
:app -.-> :core:database
|
||||
:app -.-> :core:datastore
|
||||
:app -.-> :core:di
|
||||
:app -.-> :core:domain
|
||||
:app -.-> :core:model
|
||||
:app -.-> :core:navigation
|
||||
:app -.-> :core:network
|
||||
|
|
@ -42,20 +42,27 @@ graph TB
|
|||
:app -.-> :core:resources
|
||||
:app -.-> :core:ui
|
||||
:app -.-> :core:barcode
|
||||
:app -.-> :core:takserver
|
||||
:app -.-> :feature:intro
|
||||
:app -.-> :feature:messaging
|
||||
:app -.-> :feature:connections
|
||||
:app -.-> :feature:map
|
||||
:app -.-> :feature:node
|
||||
:app -.-> :feature:settings
|
||||
:app -.-> :feature:firmware
|
||||
:app -.-> :feature:wifi-provision
|
||||
:app -.-> :feature:widget
|
||||
|
||||
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef compose-desktop-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef kmp-library-compose fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
|
||||
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
|
||||
|
||||
|
|
|
|||
|
|
@ -29,11 +29,11 @@ plugins {
|
|||
alias(libs.plugins.meshtastic.android.application)
|
||||
alias(libs.plugins.meshtastic.android.application.flavors)
|
||||
alias(libs.plugins.meshtastic.android.application.compose)
|
||||
alias(libs.plugins.meshtastic.hilt)
|
||||
id("meshtastic.koin")
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
alias(libs.plugins.devtools.ksp)
|
||||
alias(libs.plugins.secrets)
|
||||
alias(libs.plugins.aboutlibraries)
|
||||
id("dev.mokkery")
|
||||
}
|
||||
|
||||
val keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
|
|
@ -44,7 +44,7 @@ if (keystorePropertiesFile.exists()) {
|
|||
}
|
||||
|
||||
configure<ApplicationExtension> {
|
||||
namespace = configProperties.getProperty("APPLICATION_ID")
|
||||
namespace = "org.meshtastic.app"
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
|
|
@ -150,7 +150,7 @@ configure<ApplicationExtension> {
|
|||
includeInBundle = false
|
||||
}
|
||||
|
||||
testInstrumentationRunner = "com.geeksville.mesh.TestRunner"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
// Configure existing product flavors (defined by convention plugin)
|
||||
|
|
@ -171,8 +171,6 @@ configure<ApplicationExtension> {
|
|||
} else {
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
isDebuggable = false
|
||||
}
|
||||
}
|
||||
|
|
@ -210,15 +208,16 @@ project.afterEvaluate {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.ble)
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.data)
|
||||
implementation(projects.core.database)
|
||||
implementation(projects.core.datastore)
|
||||
implementation(projects.core.di)
|
||||
implementation(projects.core.domain)
|
||||
implementation(projects.core.model)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(libs.jetbrains.lifecycle.viewmodel.navigation3)
|
||||
implementation(projects.core.network)
|
||||
implementation(projects.core.nfc)
|
||||
implementation(projects.core.prefs)
|
||||
|
|
@ -227,83 +226,113 @@ dependencies {
|
|||
implementation(projects.core.resources)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.barcode)
|
||||
implementation(projects.core.takserver)
|
||||
implementation(projects.feature.intro)
|
||||
implementation(projects.feature.messaging)
|
||||
implementation(projects.feature.connections)
|
||||
implementation(projects.feature.map)
|
||||
implementation(projects.feature.node)
|
||||
implementation(projects.feature.settings)
|
||||
implementation(projects.feature.firmware)
|
||||
implementation(projects.feature.wifiProvision)
|
||||
implementation(projects.feature.widget)
|
||||
|
||||
implementation(libs.androidx.compose.material3.adaptive)
|
||||
implementation(libs.androidx.compose.material3.adaptive.layout)
|
||||
implementation(libs.androidx.compose.material3.adaptive.navigation)
|
||||
implementation(libs.androidx.compose.material3.navigationSuite)
|
||||
implementation(libs.jetbrains.compose.material3.adaptive)
|
||||
implementation(libs.jetbrains.compose.material3.adaptive.layout)
|
||||
implementation(libs.jetbrains.compose.material3.adaptive.navigation)
|
||||
implementation(libs.material)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material.iconsExtended)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.ui.text)
|
||||
implementation(libs.compose.multiplatform.animation)
|
||||
implementation(libs.compose.multiplatform.material3)
|
||||
implementation(libs.compose.multiplatform.ui.tooling.preview)
|
||||
implementation(libs.compose.multiplatform.ui)
|
||||
implementation(libs.androidx.glance.appwidget)
|
||||
implementation(libs.androidx.glance.appwidget.preview)
|
||||
implementation(libs.androidx.glance.material3)
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.paging.compose)
|
||||
implementation(libs.coil.network.okhttp)
|
||||
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
|
||||
implementation(libs.jetbrains.lifecycle.runtime.compose)
|
||||
implementation(libs.jetbrains.navigation3.ui)
|
||||
implementation(libs.ktor.client.android)
|
||||
implementation(libs.ktor.client.content.negotiation)
|
||||
implementation(libs.ktor.serialization.kotlinx.json)
|
||||
implementation(libs.ktor.client.logging)
|
||||
implementation(libs.coil)
|
||||
implementation(libs.coil.network.ktor3)
|
||||
implementation(libs.coil.svg)
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.org.eclipse.paho.client.mqttv3)
|
||||
implementation(libs.usb.serial.android)
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
implementation(libs.androidx.hilt.work)
|
||||
ksp(libs.androidx.hilt.compiler)
|
||||
implementation(libs.koin.android)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
implementation(libs.koin.androidx.workmanager)
|
||||
implementation(libs.koin.annotations)
|
||||
implementation(libs.accompanist.permissions)
|
||||
implementation(libs.kermit)
|
||||
|
||||
implementation(libs.nordic.client.android)
|
||||
implementation(libs.nordic.common.core)
|
||||
implementation(libs.nordic.common.permissions.ble)
|
||||
implementation(libs.nordic.common.permissions.notification)
|
||||
implementation(libs.nordic.common.scanner.ble)
|
||||
implementation(libs.nordic.common.ui)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
|
||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
debugImplementation(libs.androidx.glance.preview)
|
||||
|
||||
googleImplementation(libs.location.services)
|
||||
googleImplementation(libs.play.services.maps)
|
||||
googleImplementation(libs.maps.compose)
|
||||
googleImplementation(libs.maps.compose.utils)
|
||||
googleImplementation(libs.maps.compose.widgets)
|
||||
googleImplementation(libs.dd.sdk.android.logs)
|
||||
googleImplementation(libs.dd.sdk.android.rum)
|
||||
googleImplementation(libs.dd.sdk.android.session.replay)
|
||||
googleImplementation(libs.dd.sdk.android.session.replay.material)
|
||||
googleImplementation(libs.dd.sdk.android.timber)
|
||||
googleImplementation(libs.dd.sdk.android.trace)
|
||||
googleImplementation(libs.dd.sdk.android.trace.otel)
|
||||
googleImplementation(platform(libs.firebase.bom))
|
||||
googleImplementation(libs.firebase.analytics)
|
||||
googleImplementation(libs.firebase.crashlytics)
|
||||
|
||||
fdroidImplementation(libs.osmdroid.android)
|
||||
fdroidImplementation(libs.osmdroid.geopackage) { exclude(group = "com.j256.ormlite") }
|
||||
fdroidImplementation(libs.osmbonuspack)
|
||||
|
||||
androidTestImplementation(libs.androidx.test.runner)
|
||||
androidTestImplementation(libs.androidx.test.ext.junit)
|
||||
androidTestImplementation(libs.hilt.android.testing)
|
||||
androidTestImplementation(libs.kotlinx.coroutines.test)
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
androidTestImplementation(libs.nordic.client.android.mock)
|
||||
androidTestImplementation(libs.nordic.core.mock)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.mockk)
|
||||
testImplementation(kotlin("test-junit"))
|
||||
testImplementation(libs.androidx.work.testing)
|
||||
testImplementation(libs.koin.test)
|
||||
testRuntimeOnly(libs.junit.vintage.engine)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.nordic.client.android.mock)
|
||||
testImplementation(libs.nordic.client.core.mock)
|
||||
testImplementation(libs.nordic.core.mock)
|
||||
testImplementation(libs.robolectric)
|
||||
testImplementation(libs.androidx.test.core)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
testImplementation(libs.compose.multiplatform.ui.test)
|
||||
testImplementation(libs.androidx.test.ext.junit)
|
||||
testImplementation(libs.androidx.glance.appwidget)
|
||||
}
|
||||
|
||||
aboutLibraries {
|
||||
export { excludeFields = listOf("generated") }
|
||||
// Run offline by default to avoid burning GitHub API calls on every build.
|
||||
// Release builds pass -PaboutLibraries.release=true to fetch full license text + funding info.
|
||||
val isReleaseBuild = providers.gradleProperty("aboutLibraries.release").map { it.toBoolean() }.getOrElse(false)
|
||||
val ghToken = providers.environmentVariable("GITHUB_TOKEN")
|
||||
|
||||
offlineMode = !isReleaseBuild
|
||||
|
||||
collect {
|
||||
fetchRemoteLicense = isReleaseBuild && ghToken.isPresent
|
||||
fetchRemoteFunding = isReleaseBuild && ghToken.isPresent
|
||||
if (ghToken.isPresent) {
|
||||
gitHubApiToken = ghToken.get()
|
||||
}
|
||||
}
|
||||
export {
|
||||
excludeFields = listOf("generated")
|
||||
outputFile = file("src/main/resources/aboutlibraries.json")
|
||||
}
|
||||
library {
|
||||
duplicationMode = DuplicateMode.MERGE
|
||||
duplicationRule = DuplicateRule.SIMPLE
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure aboutlibraries.json is always up-to-date during the build.
|
||||
// This is required since AboutLibraries v11+ no longer auto-exports.
|
||||
tasks
|
||||
.matching { it.name.startsWith("process") && it.name.endsWith("Resources") }
|
||||
.configureEach { dependsOn("exportLibraryDefinitions") }
|
||||
|
|
|
|||
|
|
@ -1,415 +0,0 @@
|
|||
androidx.activity:activity-compose:1.12.3
|
||||
androidx.activity:activity-ktx:1.12.3
|
||||
androidx.activity:activity:1.12.3
|
||||
androidx.annotation:annotation-experimental:1.5.1
|
||||
androidx.annotation:annotation-jvm:1.9.1
|
||||
androidx.annotation:annotation:1.9.1
|
||||
androidx.appcompat:appcompat-resources:1.7.1
|
||||
androidx.appcompat:appcompat:1.7.1
|
||||
androidx.arch.core:core-common:2.2.0
|
||||
androidx.arch.core:core-runtime:2.2.0
|
||||
androidx.autofill:autofill:1.0.0
|
||||
androidx.cardview:cardview:1.0.0
|
||||
androidx.collection:collection-jvm:1.5.0
|
||||
androidx.collection:collection-ktx:1.5.0
|
||||
androidx.collection:collection:1.5.0
|
||||
androidx.compose.animation:animation-android:1.11.0-alpha04
|
||||
androidx.compose.animation:animation-core-android:1.11.0-alpha04
|
||||
androidx.compose.animation:animation-core:1.11.0-alpha04
|
||||
androidx.compose.animation:animation:1.11.0-alpha04
|
||||
androidx.compose.foundation:foundation-android:1.11.0-alpha04
|
||||
androidx.compose.foundation:foundation-layout-android:1.11.0-alpha04
|
||||
androidx.compose.foundation:foundation-layout:1.11.0-alpha04
|
||||
androidx.compose.foundation:foundation:1.11.0-alpha04
|
||||
androidx.compose.material3.adaptive:adaptive-android:1.3.0-alpha07
|
||||
androidx.compose.material3.adaptive:adaptive-layout-android:1.3.0-alpha07
|
||||
androidx.compose.material3.adaptive:adaptive-layout:1.3.0-alpha07
|
||||
androidx.compose.material3.adaptive:adaptive-navigation-android:1.3.0-alpha07
|
||||
androidx.compose.material3.adaptive:adaptive-navigation:1.3.0-alpha07
|
||||
androidx.compose.material3.adaptive:adaptive:1.3.0-alpha07
|
||||
androidx.compose.material3:material3-adaptive-navigation-suite-android:1.5.0-alpha13
|
||||
androidx.compose.material3:material3-adaptive-navigation-suite:1.5.0-alpha13
|
||||
androidx.compose.material3:material3-android:1.5.0-alpha13
|
||||
androidx.compose.material3:material3:1.5.0-alpha13
|
||||
androidx.compose.material:material-android:1.11.0-alpha04
|
||||
androidx.compose.material:material-icons-core-android:1.7.8
|
||||
androidx.compose.material:material-icons-core:1.7.8
|
||||
androidx.compose.material:material-icons-extended-android:1.7.8
|
||||
androidx.compose.material:material-icons-extended:1.7.8
|
||||
androidx.compose.material:material-ripple-android:1.11.0-alpha04
|
||||
androidx.compose.material:material-ripple:1.11.0-alpha04
|
||||
androidx.compose.material:material:1.11.0-alpha04
|
||||
androidx.compose.runtime:runtime-android:1.11.0-alpha04
|
||||
androidx.compose.runtime:runtime-annotation-android:1.11.0-alpha04
|
||||
androidx.compose.runtime:runtime-annotation:1.11.0-alpha04
|
||||
androidx.compose.runtime:runtime-livedata:1.11.0-alpha04
|
||||
androidx.compose.runtime:runtime-retain-android:1.11.0-alpha04
|
||||
androidx.compose.runtime:runtime-retain:1.11.0-alpha04
|
||||
androidx.compose.runtime:runtime-saveable-android:1.11.0-alpha04
|
||||
androidx.compose.runtime:runtime-saveable:1.11.0-alpha04
|
||||
androidx.compose.runtime:runtime-tracing:1.11.0-alpha04
|
||||
androidx.compose.runtime:runtime:1.11.0-alpha04
|
||||
androidx.compose.ui:ui-android:1.11.0-alpha04
|
||||
androidx.compose.ui:ui-geometry-android:1.11.0-alpha04
|
||||
androidx.compose.ui:ui-geometry:1.11.0-alpha04
|
||||
androidx.compose.ui:ui-graphics-android:1.11.0-alpha04
|
||||
androidx.compose.ui:ui-graphics:1.11.0-alpha04
|
||||
androidx.compose.ui:ui-text-android:1.11.0-alpha04
|
||||
androidx.compose.ui:ui-text:1.11.0-alpha04
|
||||
androidx.compose.ui:ui-tooling-android:1.11.0-alpha04
|
||||
androidx.compose.ui:ui-tooling-data-android:1.11.0-alpha04
|
||||
androidx.compose.ui:ui-tooling-data:1.11.0-alpha04
|
||||
androidx.compose.ui:ui-tooling-preview-android:1.11.0-alpha04
|
||||
androidx.compose.ui:ui-tooling-preview:1.11.0-alpha04
|
||||
androidx.compose.ui:ui-tooling:1.11.0-alpha04
|
||||
androidx.compose.ui:ui-unit-android:1.11.0-alpha04
|
||||
androidx.compose.ui:ui-unit:1.11.0-alpha04
|
||||
androidx.compose.ui:ui-util-android:1.11.0-alpha04
|
||||
androidx.compose.ui:ui-util:1.11.0-alpha04
|
||||
androidx.compose.ui:ui:1.11.0-alpha04
|
||||
androidx.compose:compose-bom-alpha:2026.01.01
|
||||
androidx.compose:compose-bom:2026.01.00
|
||||
androidx.concurrent:concurrent-futures-ktx:1.1.0
|
||||
androidx.concurrent:concurrent-futures:1.1.0
|
||||
androidx.constraintlayout:constraintlayout-core:1.0.0
|
||||
androidx.constraintlayout:constraintlayout:2.1.0
|
||||
androidx.coordinatorlayout:coordinatorlayout:1.1.0
|
||||
androidx.core:core-ktx:1.17.0
|
||||
androidx.core:core-location-altitude-external-protobuf:1.0.0-beta01
|
||||
androidx.core:core-location-altitude-proto:1.0.0-beta01
|
||||
androidx.core:core-location-altitude:1.0.0-beta01
|
||||
androidx.core:core-splashscreen:1.2.0
|
||||
androidx.core:core-viewtree:1.0.0
|
||||
androidx.core:core:1.17.0
|
||||
androidx.cursoradapter:cursoradapter:1.0.0
|
||||
androidx.customview:customview-poolingcontainer:1.0.0
|
||||
androidx.customview:customview:1.1.0
|
||||
androidx.databinding:viewbinding:8.13.2
|
||||
androidx.datastore:datastore-android:1.2.0
|
||||
androidx.datastore:datastore-core-android:1.2.0
|
||||
androidx.datastore:datastore-core-okio-jvm:1.2.0
|
||||
androidx.datastore:datastore-core-okio:1.2.0
|
||||
androidx.datastore:datastore-core:1.2.0
|
||||
androidx.datastore:datastore-preferences-android:1.2.0
|
||||
androidx.datastore:datastore-preferences-core-android:1.2.0
|
||||
androidx.datastore:datastore-preferences-core:1.2.0
|
||||
androidx.datastore:datastore-preferences-external-protobuf:1.2.0
|
||||
androidx.datastore:datastore-preferences-proto:1.2.0
|
||||
androidx.datastore:datastore-preferences:1.2.0
|
||||
androidx.datastore:datastore:1.2.0
|
||||
androidx.documentfile:documentfile:1.0.0
|
||||
androidx.drawerlayout:drawerlayout:1.1.1
|
||||
androidx.dynamicanimation:dynamicanimation:1.1.0
|
||||
androidx.emoji2:emoji2-emojipicker:1.6.0
|
||||
androidx.emoji2:emoji2-views-helper:1.6.0
|
||||
androidx.emoji2:emoji2:1.6.0
|
||||
androidx.exifinterface:exifinterface:1.4.1
|
||||
androidx.fragment:fragment-ktx:1.6.2
|
||||
androidx.fragment:fragment:1.6.2
|
||||
androidx.graphics:graphics-path:1.0.1
|
||||
androidx.graphics:graphics-shapes-android:1.0.1
|
||||
androidx.graphics:graphics-shapes:1.0.1
|
||||
androidx.hilt:hilt-common:1.3.0
|
||||
androidx.hilt:hilt-lifecycle-viewmodel-compose:1.3.0
|
||||
androidx.hilt:hilt-lifecycle-viewmodel:1.3.0
|
||||
androidx.hilt:hilt-work:1.3.0
|
||||
androidx.interpolator:interpolator:1.0.0
|
||||
androidx.legacy:legacy-support-core-utils:1.0.0
|
||||
androidx.lifecycle:lifecycle-common-java8:2.10.0
|
||||
androidx.lifecycle:lifecycle-common-jvm:2.10.0
|
||||
androidx.lifecycle:lifecycle-common:2.10.0
|
||||
androidx.lifecycle:lifecycle-livedata-core-ktx:2.10.0
|
||||
androidx.lifecycle:lifecycle-livedata-core:2.10.0
|
||||
androidx.lifecycle:lifecycle-livedata-ktx:2.10.0
|
||||
androidx.lifecycle:lifecycle-livedata:2.10.0
|
||||
androidx.lifecycle:lifecycle-process:2.10.0
|
||||
androidx.lifecycle:lifecycle-runtime-android:2.10.0
|
||||
androidx.lifecycle:lifecycle-runtime-compose-android:2.10.0
|
||||
androidx.lifecycle:lifecycle-runtime-compose:2.10.0
|
||||
androidx.lifecycle:lifecycle-runtime-ktx-android:2.10.0
|
||||
androidx.lifecycle:lifecycle-runtime-ktx:2.10.0
|
||||
androidx.lifecycle:lifecycle-runtime:2.10.0
|
||||
androidx.lifecycle:lifecycle-service:2.10.0
|
||||
androidx.lifecycle:lifecycle-viewmodel-android:2.10.0
|
||||
androidx.lifecycle:lifecycle-viewmodel-compose-android:2.10.0
|
||||
androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0
|
||||
androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0
|
||||
androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.10.0
|
||||
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.10.0
|
||||
androidx.lifecycle:lifecycle-viewmodel:2.10.0
|
||||
androidx.loader:loader:1.0.0
|
||||
androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
|
||||
androidx.metrics:metrics-performance:1.0.0-beta03
|
||||
androidx.navigation3:navigation3-runtime-android:1.0.0
|
||||
androidx.navigation3:navigation3-runtime:1.0.0
|
||||
androidx.navigation3:navigation3-ui-android:1.0.0
|
||||
androidx.navigation3:navigation3-ui:1.0.0
|
||||
androidx.navigation:navigation-common-android:2.9.7
|
||||
androidx.navigation:navigation-common:2.9.7
|
||||
androidx.navigation:navigation-compose-android:2.9.7
|
||||
androidx.navigation:navigation-compose:2.9.7
|
||||
androidx.navigation:navigation-fragment:2.9.7
|
||||
androidx.navigation:navigation-runtime-android:2.9.7
|
||||
androidx.navigation:navigation-runtime:2.9.7
|
||||
androidx.navigationevent:navigationevent-android:1.0.2
|
||||
androidx.navigationevent:navigationevent-compose-android:1.0.2
|
||||
androidx.navigationevent:navigationevent-compose:1.0.2
|
||||
androidx.navigationevent:navigationevent:1.0.2
|
||||
androidx.paging:paging-common-android:3.4.0
|
||||
androidx.paging:paging-common:3.4.0
|
||||
androidx.paging:paging-compose-android:3.4.0
|
||||
androidx.paging:paging-compose:3.4.0
|
||||
androidx.print:print:1.0.0
|
||||
androidx.privacysandbox.ads:ads-adservices-java:1.1.0-beta11
|
||||
androidx.privacysandbox.ads:ads-adservices:1.1.0-beta11
|
||||
androidx.profileinstaller:profileinstaller:1.4.1
|
||||
androidx.recyclerview:recyclerview:1.3.2
|
||||
androidx.resourceinspection:resourceinspection-annotation:1.0.1
|
||||
androidx.room:room-common-jvm:2.8.4
|
||||
androidx.room:room-common:2.8.4
|
||||
androidx.room:room-paging-android:2.8.4
|
||||
androidx.room:room-paging:2.8.4
|
||||
androidx.room:room-runtime-android:2.8.4
|
||||
androidx.room:room-runtime:2.8.4
|
||||
androidx.savedstate:savedstate-android:1.4.0
|
||||
androidx.savedstate:savedstate-compose-android:1.4.0
|
||||
androidx.savedstate:savedstate-compose:1.4.0
|
||||
androidx.savedstate:savedstate-ktx:1.4.0
|
||||
androidx.savedstate:savedstate:1.4.0
|
||||
androidx.slidingpanelayout:slidingpanelayout:1.2.0
|
||||
androidx.sqlite:sqlite-android:2.6.2
|
||||
androidx.sqlite:sqlite-framework-android:2.6.2
|
||||
androidx.sqlite:sqlite-framework:2.6.2
|
||||
androidx.sqlite:sqlite:2.6.2
|
||||
androidx.startup:startup-runtime:1.2.0
|
||||
androidx.tracing:tracing-ktx:1.2.0
|
||||
androidx.tracing:tracing-perfetto:1.0.1
|
||||
androidx.tracing:tracing:1.2.0
|
||||
androidx.transition:transition:1.6.0
|
||||
androidx.vectordrawable:vectordrawable-animated:1.1.0
|
||||
androidx.vectordrawable:vectordrawable:1.1.0
|
||||
androidx.versionedparcelable:versionedparcelable:1.1.1
|
||||
androidx.viewpager2:viewpager2:1.1.0-beta02
|
||||
androidx.viewpager:viewpager:1.0.0
|
||||
androidx.window:window-core-android:1.5.0
|
||||
androidx.window:window-core:1.5.0
|
||||
androidx.window:window:1.5.0
|
||||
androidx.work:work-runtime-ktx:2.11.1
|
||||
androidx.work:work-runtime:2.11.1
|
||||
co.touchlab:kermit-android:2.0.8
|
||||
co.touchlab:kermit-core-android:2.0.8
|
||||
co.touchlab:kermit-core:2.0.8
|
||||
co.touchlab:kermit:2.0.8
|
||||
com.caverock:androidsvg-aar:1.4
|
||||
com.datadoghq:dd-sdk-android-compose:3.6.0
|
||||
com.datadoghq:dd-sdk-android-core:3.6.0
|
||||
com.datadoghq:dd-sdk-android-internal:3.6.0
|
||||
com.datadoghq:dd-sdk-android-logs:3.6.0
|
||||
com.datadoghq:dd-sdk-android-okhttp:3.6.0
|
||||
com.datadoghq:dd-sdk-android-rum:3.6.0
|
||||
com.datadoghq:dd-sdk-android-session-replay-compose:3.6.0
|
||||
com.datadoghq:dd-sdk-android-session-replay:3.6.0
|
||||
com.datadoghq:dd-sdk-android-timber:3.6.0
|
||||
com.datadoghq:dd-sdk-android-trace-api:3.6.0
|
||||
com.datadoghq:dd-sdk-android-trace-internal:3.6.0
|
||||
com.datadoghq:dd-sdk-android-trace-otel:3.6.0
|
||||
com.datadoghq:dd-sdk-android-trace:3.6.0
|
||||
com.github.mik3y:usb-serial-for-android:3.10.0
|
||||
com.google.accompanist:accompanist-drawablepainter:0.37.3
|
||||
com.google.accompanist:accompanist-permissions:0.37.3
|
||||
com.google.android.datatransport:transport-api:3.2.0
|
||||
com.google.android.datatransport:transport-backend-cct:3.3.0
|
||||
com.google.android.datatransport:transport-runtime:3.3.0
|
||||
com.google.android.gms:play-services-ads-identifier:18.0.0
|
||||
com.google.android.gms:play-services-base:18.5.0
|
||||
com.google.android.gms:play-services-basement:18.9.0
|
||||
com.google.android.gms:play-services-location:21.3.0
|
||||
com.google.android.gms:play-services-maps:20.0.0
|
||||
com.google.android.gms:play-services-measurement-api:23.0.0
|
||||
com.google.android.gms:play-services-measurement-base:23.0.0
|
||||
com.google.android.gms:play-services-measurement-impl:23.0.0
|
||||
com.google.android.gms:play-services-measurement-sdk-api:23.0.0
|
||||
com.google.android.gms:play-services-measurement-sdk:23.0.0
|
||||
com.google.android.gms:play-services-measurement:23.0.0
|
||||
com.google.android.gms:play-services-stats:17.0.2
|
||||
com.google.android.gms:play-services-tasks:18.4.0
|
||||
com.google.android.material:material:1.13.0
|
||||
com.google.auto.value:auto-value-annotations:1.6.3
|
||||
com.google.code.findbugs:jsr305:3.0.2
|
||||
com.google.code.gson:gson:2.13.2
|
||||
com.google.dagger:dagger-lint-aar:2.59
|
||||
com.google.dagger:dagger:2.59
|
||||
com.google.dagger:hilt-android:2.59
|
||||
com.google.dagger:hilt-core:2.59
|
||||
com.google.errorprone:error_prone_annotations:2.41.0
|
||||
com.google.firebase:firebase-analytics:23.0.0
|
||||
com.google.firebase:firebase-annotations:17.0.0
|
||||
com.google.firebase:firebase-bom:34.8.0
|
||||
com.google.firebase:firebase-common:22.0.1
|
||||
com.google.firebase:firebase-components:19.0.0
|
||||
com.google.firebase:firebase-config-interop:16.0.1
|
||||
com.google.firebase:firebase-crashlytics:20.0.4
|
||||
com.google.firebase:firebase-datatransport:19.0.0
|
||||
com.google.firebase:firebase-encoders-json:18.0.1
|
||||
com.google.firebase:firebase-encoders-proto:16.0.0
|
||||
com.google.firebase:firebase-encoders:17.0.0
|
||||
com.google.firebase:firebase-installations-interop:17.2.0
|
||||
com.google.firebase:firebase-installations:19.0.1
|
||||
com.google.firebase:firebase-measurement-connector:20.0.1
|
||||
com.google.firebase:firebase-sessions:3.0.4
|
||||
com.google.guava:failureaccess:1.0.3
|
||||
com.google.guava:guava:33.5.0-android
|
||||
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
|
||||
com.google.j2objc:j2objc-annotations:3.1
|
||||
com.google.maps.android:android-maps-utils:4.0.0
|
||||
com.google.maps.android:maps-compose-utils:8.0.0
|
||||
com.google.maps.android:maps-compose-widgets:8.0.0
|
||||
com.google.maps.android:maps-compose:8.0.0
|
||||
com.google.maps.android:maps-ktx:6.0.0
|
||||
com.google.maps.android:maps-utils-ktx:6.0.0
|
||||
com.google.re2j:re2j:1.7
|
||||
com.google.zxing:core:3.5.4
|
||||
com.jakewharton.timber:timber:5.0.1
|
||||
com.journeyapps:zxing-android-embedded:4.3.0
|
||||
com.lyft.kronos:kronos-android:0.0.1-alpha11
|
||||
com.lyft.kronos:kronos-java:0.0.1-alpha11
|
||||
com.mikepenz:aboutlibraries-compose-core-android:13.2.1
|
||||
com.mikepenz:aboutlibraries-compose-core:13.2.1
|
||||
com.mikepenz:aboutlibraries-compose-m3-android:13.2.1
|
||||
com.mikepenz:aboutlibraries-compose-m3:13.2.1
|
||||
com.mikepenz:aboutlibraries-core-android:13.2.1
|
||||
com.mikepenz:aboutlibraries-core:13.2.1
|
||||
com.mikepenz:multiplatform-markdown-renderer-android:0.39.2
|
||||
com.mikepenz:multiplatform-markdown-renderer-m3-android:0.39.2
|
||||
com.mikepenz:multiplatform-markdown-renderer-m3:0.39.2
|
||||
com.mikepenz:multiplatform-markdown-renderer:0.39.2
|
||||
com.patrykandpatrick.vico:compose-android:3.0.0-beta.3
|
||||
com.patrykandpatrick.vico:compose-m2-android:3.0.0-beta.3
|
||||
com.patrykandpatrick.vico:compose-m2:3.0.0-beta.3
|
||||
com.patrykandpatrick.vico:compose-m3-android:3.0.0-beta.3
|
||||
com.patrykandpatrick.vico:compose-m3:3.0.0-beta.3
|
||||
com.patrykandpatrick.vico:compose:3.0.0-beta.3
|
||||
com.squareup.okhttp3:logging-interceptor:5.3.2
|
||||
com.squareup.okhttp3:okhttp-android:5.3.2
|
||||
com.squareup.okhttp3:okhttp:5.3.2
|
||||
com.squareup.okio:okio-jvm:3.16.4
|
||||
com.squareup.okio:okio:3.16.4
|
||||
com.squareup.wire:wire-runtime-jvm:5.2.1
|
||||
com.squareup.wire:wire-runtime:5.2.1
|
||||
io.coil-kt.coil3:coil-android:3.3.0
|
||||
io.coil-kt.coil3:coil-compose-android:3.3.0
|
||||
io.coil-kt.coil3:coil-compose-core-android:3.3.0
|
||||
io.coil-kt.coil3:coil-compose-core:3.3.0
|
||||
io.coil-kt.coil3:coil-compose:3.3.0
|
||||
io.coil-kt.coil3:coil-core-android:3.3.0
|
||||
io.coil-kt.coil3:coil-core:3.3.0
|
||||
io.coil-kt.coil3:coil-network-core-android:3.3.0
|
||||
io.coil-kt.coil3:coil-network-core:3.3.0
|
||||
io.coil-kt.coil3:coil-network-okhttp-jvm:3.3.0
|
||||
io.coil-kt.coil3:coil-network-okhttp:3.3.0
|
||||
io.coil-kt.coil3:coil-svg-android:3.3.0
|
||||
io.coil-kt.coil3:coil-svg:3.3.0
|
||||
io.coil-kt.coil3:coil:3.3.0
|
||||
io.ktor:ktor-client-content-negotiation-jvm:3.4.0
|
||||
io.ktor:ktor-client-content-negotiation:3.4.0
|
||||
io.ktor:ktor-client-core-jvm:3.4.0
|
||||
io.ktor:ktor-client-core:3.4.0
|
||||
io.ktor:ktor-client-okhttp-jvm:3.4.0
|
||||
io.ktor:ktor-client-okhttp:3.4.0
|
||||
io.ktor:ktor-events-jvm:3.4.0
|
||||
io.ktor:ktor-events:3.4.0
|
||||
io.ktor:ktor-http-cio-jvm:3.4.0
|
||||
io.ktor:ktor-http-cio:3.4.0
|
||||
io.ktor:ktor-http-jvm:3.4.0
|
||||
io.ktor:ktor-http:3.4.0
|
||||
io.ktor:ktor-io-jvm:3.4.0
|
||||
io.ktor:ktor-io:3.4.0
|
||||
io.ktor:ktor-network-jvm:3.4.0
|
||||
io.ktor:ktor-network:3.4.0
|
||||
io.ktor:ktor-serialization-jvm:3.4.0
|
||||
io.ktor:ktor-serialization-kotlinx-json-jvm:3.4.0
|
||||
io.ktor:ktor-serialization-kotlinx-json:3.4.0
|
||||
io.ktor:ktor-serialization-kotlinx-jvm:3.4.0
|
||||
io.ktor:ktor-serialization-kotlinx:3.4.0
|
||||
io.ktor:ktor-serialization:3.4.0
|
||||
io.ktor:ktor-sse-jvm:3.4.0
|
||||
io.ktor:ktor-sse:3.4.0
|
||||
io.ktor:ktor-utils-jvm:3.4.0
|
||||
io.ktor:ktor-utils:3.4.0
|
||||
io.ktor:ktor-websocket-serialization-jvm:3.4.0
|
||||
io.ktor:ktor-websocket-serialization:3.4.0
|
||||
io.ktor:ktor-websockets-jvm:3.4.0
|
||||
io.ktor:ktor-websockets:3.4.0
|
||||
io.opentelemetry:opentelemetry-api:1.40.0
|
||||
io.opentelemetry:opentelemetry-context:1.40.0
|
||||
jakarta.inject:jakarta.inject-api:2.0.1
|
||||
javax.inject:javax.inject:1
|
||||
no.nordicsemi.android:dfu:2.10.1
|
||||
no.nordicsemi.kotlin.ble:client-android:2.0.0-alpha12
|
||||
no.nordicsemi.kotlin.ble:client-core-android:2.0.0-alpha12
|
||||
no.nordicsemi.kotlin.ble:client-core:2.0.0-alpha12
|
||||
no.nordicsemi.kotlin.ble:core:2.0.0-alpha12
|
||||
org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5
|
||||
org.jctools:jctools-core:3.3.0
|
||||
org.jetbrains.androidx.lifecycle:lifecycle-common:2.9.6
|
||||
org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.9.6
|
||||
org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.9.6
|
||||
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.6
|
||||
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.6
|
||||
org.jetbrains.androidx.savedstate:savedstate-compose:1.3.6
|
||||
org.jetbrains.androidx.savedstate:savedstate:1.3.6
|
||||
org.jetbrains.compose.animation:animation-core:1.10.0
|
||||
org.jetbrains.compose.animation:animation:1.10.0
|
||||
org.jetbrains.compose.annotation-internal:annotation:1.10.0
|
||||
org.jetbrains.compose.collection-internal:collection:1.10.0
|
||||
org.jetbrains.compose.components:components-resources-android:1.10.0
|
||||
org.jetbrains.compose.components:components-resources:1.10.0
|
||||
org.jetbrains.compose.foundation:foundation-layout:1.10.0
|
||||
org.jetbrains.compose.foundation:foundation:1.10.0
|
||||
org.jetbrains.compose.material3:material3:1.9.0
|
||||
org.jetbrains.compose.material:material-ripple:1.10.0
|
||||
org.jetbrains.compose.material:material:1.10.0
|
||||
org.jetbrains.compose.runtime:runtime-saveable:1.10.0
|
||||
org.jetbrains.compose.runtime:runtime:1.10.0
|
||||
org.jetbrains.compose.ui:ui-backhandler-android:1.9.1
|
||||
org.jetbrains.compose.ui:ui-backhandler:1.9.1
|
||||
org.jetbrains.compose.ui:ui-geometry:1.10.0
|
||||
org.jetbrains.compose.ui:ui-graphics:1.10.0
|
||||
org.jetbrains.compose.ui:ui-text:1.10.0
|
||||
org.jetbrains.compose.ui:ui-tooling-preview:1.10.0-rc02
|
||||
org.jetbrains.compose.ui:ui-unit:1.10.0
|
||||
org.jetbrains.compose.ui:ui-util:1.10.0
|
||||
org.jetbrains.compose.ui:ui:1.10.0
|
||||
org.jetbrains.kotlin:kotlin-bom:1.8.22
|
||||
org.jetbrains.kotlin:kotlin-parcelize-runtime:2.3.0
|
||||
org.jetbrains.kotlin:kotlin-stdlib-common:2.3.0
|
||||
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.3.0
|
||||
org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.21
|
||||
org.jetbrains.kotlin:kotlin-stdlib:2.3.0
|
||||
org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.4.0
|
||||
org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0
|
||||
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2
|
||||
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2
|
||||
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.2
|
||||
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2
|
||||
org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.10.2
|
||||
org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2
|
||||
org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.10.2
|
||||
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.7.1-0.6.x-compat
|
||||
org.jetbrains.kotlinx:kotlinx-datetime:0.7.1-0.6.x-compat
|
||||
org.jetbrains.kotlinx:kotlinx-io-bytestring-jvm:0.8.2
|
||||
org.jetbrains.kotlinx:kotlinx-io-bytestring:0.8.2
|
||||
org.jetbrains.kotlinx:kotlinx-io-core-jvm:0.8.2
|
||||
org.jetbrains.kotlinx:kotlinx-io-core:0.8.2
|
||||
org.jetbrains.kotlinx:kotlinx-serialization-bom:1.10.0
|
||||
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.10.0
|
||||
org.jetbrains.kotlinx:kotlinx-serialization-core:1.10.0
|
||||
org.jetbrains.kotlinx:kotlinx-serialization-json-io-jvm:1.10.0
|
||||
org.jetbrains.kotlinx:kotlinx-serialization-json-io:1.10.0
|
||||
org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.10.0
|
||||
org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0
|
||||
org.jetbrains:annotations:23.0.0
|
||||
org.jetbrains:markdown-jvm:0.7.3
|
||||
org.jetbrains:markdown:0.7.3
|
||||
org.jspecify:jspecify:1.0.0
|
||||
org.slf4j:slf4j-api:2.0.17
|
||||
|
|
@ -1,92 +1,5 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>CommentSpacing:Coroutines.kt$/// Wrap launch with an exception handler, FIXME, move into a utility lib</ID>
|
||||
<ID>CyclomaticComplexMethod:BleError.kt$BleError.Companion$fun from(exception: Throwable): BleError</ID>
|
||||
<ID>CyclomaticComplexMethod:MeshMessageProcessor.kt$MeshMessageProcessor$private fun processReceivedMeshPacket(packet: MeshPacket, myNodeNum: Int?)</ID>
|
||||
<ID>CyclomaticComplexMethod:SettingsNavigation.kt$@Suppress("LongMethod") fun NavGraphBuilder.settingsGraph(navController: NavHostController)</ID>
|
||||
<ID>EmptyClassBlock:DebugLogFile.kt$BinaryLogFile${ }</ID>
|
||||
<ID>EmptyFunctionBlock:NopInterface.kt$NopInterface${ }</ID>
|
||||
<ID>EmptyFunctionBlock:TrustAllX509TrustManager.kt$TrustAllX509TrustManager${}</ID>
|
||||
<ID>FinalNewline:Coroutines.kt$com.geeksville.mesh.concurrent.Coroutines.kt</ID>
|
||||
<ID>FinalNewline:DateUtils.kt$com.geeksville.mesh.android.DateUtils.kt</ID>
|
||||
<ID>FinalNewline:DebugLogFile.kt$com.geeksville.mesh.android.DebugLogFile.kt</ID>
|
||||
<ID>FinalNewline:InterfaceId.kt$com.geeksville.mesh.repository.radio.InterfaceId.kt</ID>
|
||||
<ID>FinalNewline:InterfaceSpec.kt$com.geeksville.mesh.repository.radio.InterfaceSpec.kt</ID>
|
||||
<ID>FinalNewline:MockInterfaceFactory.kt$com.geeksville.mesh.repository.radio.MockInterfaceFactory.kt</ID>
|
||||
<ID>FinalNewline:NopInterface.kt$com.geeksville.mesh.repository.radio.NopInterface.kt</ID>
|
||||
<ID>FinalNewline:NopInterfaceFactory.kt$com.geeksville.mesh.repository.radio.NopInterfaceFactory.kt</ID>
|
||||
<ID>FinalNewline:ProbeTableProvider.kt$com.geeksville.mesh.repository.usb.ProbeTableProvider.kt</ID>
|
||||
<ID>FinalNewline:SerialConnection.kt$com.geeksville.mesh.repository.usb.SerialConnection.kt</ID>
|
||||
<ID>FinalNewline:SerialConnectionListener.kt$com.geeksville.mesh.repository.usb.SerialConnectionListener.kt</ID>
|
||||
<ID>FinalNewline:SerialInterfaceFactory.kt$com.geeksville.mesh.repository.radio.SerialInterfaceFactory.kt</ID>
|
||||
<ID>FinalNewline:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt</ID>
|
||||
<ID>FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
|
||||
<ID>LongMethod:TCPInterface.kt$TCPInterface$private suspend fun startConnect()</ID>
|
||||
<ID>MagicNumber:Contacts.kt$7</ID>
|
||||
<ID>MagicNumber:Contacts.kt$8</ID>
|
||||
<ID>MagicNumber:MQTTRepository.kt$MQTTRepository$512</ID>
|
||||
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$21972</ID>
|
||||
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$32809</ID>
|
||||
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$6790</ID>
|
||||
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$9114</ID>
|
||||
<ID>MagicNumber:SerialConnectionImpl.kt$SerialConnectionImpl$115200</ID>
|
||||
<ID>MagicNumber:SerialConnectionImpl.kt$SerialConnectionImpl$200</ID>
|
||||
<ID>MagicNumber:ServiceClient.kt$ServiceClient$500</ID>
|
||||
<ID>MagicNumber:StreamInterface.kt$StreamInterface$0xff</ID>
|
||||
<ID>MagicNumber:StreamInterface.kt$StreamInterface$3</ID>
|
||||
<ID>MagicNumber:StreamInterface.kt$StreamInterface$4</ID>
|
||||
<ID>MagicNumber:StreamInterface.kt$StreamInterface$8</ID>
|
||||
<ID>MagicNumber:TCPInterface.kt$TCPInterface$1000</ID>
|
||||
<ID>MagicNumber:UIState.kt$4</ID>
|
||||
<ID>MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"[$address] Found fromNum: ${fromNumCharacteristic?.uuid}, ${fromNumCharacteristic?.instanceId}"</ID>
|
||||
<ID>MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"[$address] Found fromRadio: ${fromRadioCharacteristic?.uuid}, ${fromRadioCharacteristic?.instanceId}"</ID>
|
||||
<ID>MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"[$address] Found logRadio: ${logRadioCharacteristic?.uuid}, ${logRadioCharacteristic?.instanceId}"</ID>
|
||||
<ID>MaxLineLength:NordicBleInterface.kt$NordicBleInterface$"[$address] Found toRadio: ${toRadioCharacteristic?.uuid}, ${toRadioCharacteristic?.instanceId}"</ID>
|
||||
<ID>NewLineAtEndOfFile:Coroutines.kt$com.geeksville.mesh.concurrent.Coroutines.kt</ID>
|
||||
<ID>NewLineAtEndOfFile:DateUtils.kt$com.geeksville.mesh.android.DateUtils.kt</ID>
|
||||
<ID>NewLineAtEndOfFile:DebugLogFile.kt$com.geeksville.mesh.android.DebugLogFile.kt</ID>
|
||||
<ID>NewLineAtEndOfFile:InterfaceId.kt$com.geeksville.mesh.repository.radio.InterfaceId.kt</ID>
|
||||
<ID>NewLineAtEndOfFile:InterfaceSpec.kt$com.geeksville.mesh.repository.radio.InterfaceSpec.kt</ID>
|
||||
<ID>NewLineAtEndOfFile:MockInterfaceFactory.kt$com.geeksville.mesh.repository.radio.MockInterfaceFactory.kt</ID>
|
||||
<ID>NewLineAtEndOfFile:NopInterface.kt$com.geeksville.mesh.repository.radio.NopInterface.kt</ID>
|
||||
<ID>NewLineAtEndOfFile:NopInterfaceFactory.kt$com.geeksville.mesh.repository.radio.NopInterfaceFactory.kt</ID>
|
||||
<ID>NewLineAtEndOfFile:ProbeTableProvider.kt$com.geeksville.mesh.repository.usb.ProbeTableProvider.kt</ID>
|
||||
<ID>NewLineAtEndOfFile:SerialConnection.kt$com.geeksville.mesh.repository.usb.SerialConnection.kt</ID>
|
||||
<ID>NewLineAtEndOfFile:SerialConnectionListener.kt$com.geeksville.mesh.repository.usb.SerialConnectionListener.kt</ID>
|
||||
<ID>NewLineAtEndOfFile:SerialInterfaceFactory.kt$com.geeksville.mesh.repository.radio.SerialInterfaceFactory.kt</ID>
|
||||
<ID>NewLineAtEndOfFile:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt</ID>
|
||||
<ID>NewLineAtEndOfFile:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
|
||||
<ID>NoBlankLineBeforeRbrace:DebugLogFile.kt$BinaryLogFile$ </ID>
|
||||
<ID>NoBlankLineBeforeRbrace:NopInterface.kt$NopInterface$ </ID>
|
||||
<ID>NoConsecutiveBlankLines:DebugLogFile.kt$ </ID>
|
||||
<ID>NoEmptyClassBody:DebugLogFile.kt$BinaryLogFile${ }</ID>
|
||||
<ID>NoSemicolons:DateUtils.kt$DateUtils$;</ID>
|
||||
<ID>OptionalAbstractKeyword:SyncContinuation.kt$Continuation$abstract</ID>
|
||||
<ID>RethrowCaughtException:SyncContinuation.kt$Continuation$throw ex</ID>
|
||||
<ID>ReturnCount:MeshDataHandler.kt$MeshDataHandler$@Suppress("LongMethod") private fun handleStoreForwardPlusPlus(packet: MeshPacket)</ID>
|
||||
<ID>ReturnCount:MeshDataHandler.kt$MeshDataHandler$private fun shouldBatteryNotificationShow(fromNum: Int, t: Telemetry, myNodeNum: Int): Boolean</ID>
|
||||
<ID>SwallowedException:Exceptions.kt$ex: Throwable</ID>
|
||||
<ID>SwallowedException:NsdManager.kt$ex: IllegalArgumentException</ID>
|
||||
<ID>SwallowedException:ServiceClient.kt$ServiceClient$ex: IllegalArgumentException</ID>
|
||||
<ID>SwallowedException:TCPInterface.kt$TCPInterface$ex: SocketTimeoutException</ID>
|
||||
<ID>TooGenericExceptionCaught:Exceptions.kt$ex: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:MQTTRepository.kt$MQTTRepository$ex: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:MeshDataHandler.kt$MeshDataHandler$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:MeshService.kt$MeshService$ex: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:NordicBleInterface.kt$NordicBleInterface$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:NordicBleInterface.kt$NordicBleInterface$t: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:RadioInterfaceService.kt$RadioInterfaceService$t: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:SyncContinuation.kt$Continuation$ex: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:TCPInterface.kt$TCPInterface$ex: Throwable</ID>
|
||||
<ID>TooGenericExceptionThrown:ServiceClient.kt$ServiceClient$throw Exception("Haven't called connect")</ID>
|
||||
<ID>TooGenericExceptionThrown:ServiceClient.kt$ServiceClient$throw Exception("Service not bound")</ID>
|
||||
<ID>TooGenericExceptionThrown:SyncContinuation.kt$SyncContinuation$throw Exception("SyncContinuation timeout")</ID>
|
||||
<ID>TooGenericExceptionThrown:SyncContinuation.kt$SyncContinuation$throw Exception("This shouldn't happen")</ID>
|
||||
<ID>TooManyFunctions:NordicBleInterface.kt$NordicBleInterface : IRadioInterface</ID>
|
||||
<ID>TooManyFunctions:RadioInterfaceService.kt$RadioInterfaceService</ID>
|
||||
<ID>TooManyFunctions:UIState.kt$UIViewModel : ViewModel</ID>
|
||||
<ID>UtilityClassWithPublicConstructor:NetworkRepositoryModule.kt$NetworkRepositoryModule</ID>
|
||||
</CurrentIssues>
|
||||
<CurrentIssues/>
|
||||
</SmellBaseline>
|
||||
|
|
|
|||
73
app/proguard-rules.pro
vendored
73
app/proguard-rules.pro
vendored
|
|
@ -1,50 +1,45 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.
|
||||
# ============================================================================
|
||||
# Meshtastic Android — ProGuard / R8 rules for release minification
|
||||
# ============================================================================
|
||||
# Open-source project: obfuscation and optimization are disabled. We rely on
|
||||
# tree-shaking (unused code removal) for APK size reduction.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
# Cross-platform library rules (Koin, kotlinx-serialization, Wire, Room,
|
||||
# Ktor, Coil, Kable, Kermit, Okio, DataStore, Paging, Lifecycle, Navigation 3,
|
||||
# AboutLibraries, Markdown, QRCode, CMP resources, core model) live in
|
||||
# config/proguard/shared-rules.pro and are wired in by the
|
||||
# AndroidApplicationConventionPlugin. This file holds only Android-specific
|
||||
# rules and R8-only directives.
|
||||
# ============================================================================
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
# ---- General ----------------------------------------------------------------
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
# Open-source — no need to obfuscate
|
||||
-dontobfuscate
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
# Disable R8 optimization passes. Tree-shaking (unused code removal) still
|
||||
# runs — only method-body rewrites and call-site transformations are suppressed.
|
||||
#
|
||||
# Why: CMP 1.11 ships consumer rules with -assumenosideeffects on
|
||||
# Composer.<clinit>() and ComposerImpl.<clinit>(), plus -assumevalues on
|
||||
# ComposeRuntimeFlags and ComposeStackTraceMode. These optimization directives
|
||||
# let R8 rewrite *call sites* (class-init triggers, flag reads) even when the
|
||||
# target classes are preserved by -keep rules. The result is that the Compose
|
||||
# recomposer/frame-clock/animation state machines silently freeze on their
|
||||
# first frame in release builds. -dontoptimize is the only directive that
|
||||
# disables processing of -assumenosideeffects/-assumevalues. See #5146.
|
||||
-dontoptimize
|
||||
|
||||
# Needed for protobufs
|
||||
-keep class com.google.protobuf.** { *; }
|
||||
-keep class org.meshtastic.proto.** { *; }
|
||||
# Dump the full merged R8 configuration (app rules + all library consumer rules)
|
||||
# for auditing. Inspect this file after a release build to see what libraries inject.
|
||||
-printconfiguration build/outputs/mapping/r8-merged-config.txt
|
||||
|
||||
# eclipse.paho.client
|
||||
-keep class org.eclipse.paho.client.mqttv3.logging.JSR47Logger { *; }
|
||||
# ---- Networking (transitive references from Ktor on Android) ----------------
|
||||
|
||||
# OkHttp
|
||||
-dontwarn okhttp3.internal.platform.**
|
||||
-dontwarn org.conscrypt.**
|
||||
-dontwarn org.bouncycastle.**
|
||||
-dontwarn org.openjsse.**
|
||||
|
||||
# ?
|
||||
-dontwarn java.lang.reflect.**
|
||||
-dontwarn com.google.errorprone.annotations.**
|
||||
|
||||
# Our app is opensource no need to obsfucate
|
||||
-dontobfuscate
|
||||
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*,!code/allocation/variable
|
||||
|
||||
# R8 optimization for Kotlin null checks (AGP 9.0+)
|
||||
-processkotlinnullchecks remove
|
||||
|
||||
# Nordic BLE
|
||||
-dontwarn no.nordicsemi.kotlin.ble.environment.android.mock.**
|
||||
-keep class no.nordicsemi.kotlin.ble.environment.android.mock.** { *; }
|
||||
-keep class no.nordicsemi.kotlin.ble.environment.android.compose.** { *; }
|
||||
# Compose runtime/ui/animation/foundation/material3 keep rules now live in
|
||||
# config/proguard/shared-rules.pro so both Android (R8) and desktop (ProGuard)
|
||||
# get the same defence-in-depth coverage against CMP 1.11 optimizer folding.
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* 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.geeksville.mesh.filter
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.core.prefs.filter.FilterPrefs
|
||||
import org.meshtastic.core.service.filter.MessageFilterService
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MessageFilterIntegrationTest {
|
||||
|
||||
@get:Rule var hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Inject lateinit var filterPrefs: FilterPrefs
|
||||
|
||||
@Inject lateinit var filterService: MessageFilterService
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
hiltRule.inject()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun filterPrefsIntegration() = runTest {
|
||||
filterPrefs.filterEnabled = true
|
||||
filterPrefs.filterWords = setOf("test", "spam")
|
||||
filterService.rebuildPatterns()
|
||||
|
||||
assertTrue(filterService.shouldFilter("this is a test message"))
|
||||
assertTrue(filterService.shouldFilter("spam content"))
|
||||
}
|
||||
}
|
||||
|
|
@ -15,14 +15,14 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.map.cluster;
|
||||
package org.meshtastic.app.map.cluster;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Point;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import org.meshtastic.feature.map.model.MarkerWithLabel;
|
||||
import org.meshtastic.app.map.model.MarkerWithLabel;
|
||||
|
||||
import org.osmdroid.util.BoundingBox;
|
||||
import org.osmdroid.views.MapView;
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.map.cluster;
|
||||
package org.meshtastic.app.map.cluster;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
|
|
@ -27,7 +27,7 @@ import android.graphics.drawable.BitmapDrawable;
|
|||
import android.graphics.drawable.Drawable;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import org.meshtastic.feature.map.model.MarkerWithLabel;
|
||||
import org.meshtastic.app.map.model.MarkerWithLabel;
|
||||
|
||||
import org.osmdroid.bonuspack.R;
|
||||
import org.osmdroid.util.BoundingBox;
|
||||
|
|
@ -15,9 +15,9 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.map.cluster;
|
||||
package org.meshtastic.app.map.cluster;
|
||||
|
||||
import org.meshtastic.feature.map.model.MarkerWithLabel;
|
||||
import org.meshtastic.app.map.model.MarkerWithLabel;
|
||||
|
||||
import org.osmdroid.util.BoundingBox;
|
||||
import org.osmdroid.util.GeoPoint;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* 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
|
||||
|
|
@ -14,29 +14,28 @@
|
|||
* 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 org.meshtastic.app.analytics
|
||||
|
||||
package org.meshtastic.core.analytics.platform
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.NavHostController
|
||||
import co.touchlab.kermit.Logger
|
||||
import co.touchlab.kermit.Severity
|
||||
import org.meshtastic.core.analytics.BuildConfig
|
||||
import org.meshtastic.core.analytics.DataPair
|
||||
import javax.inject.Inject
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.app.BuildConfig
|
||||
import org.meshtastic.core.repository.DataPair
|
||||
import org.meshtastic.core.repository.PlatformAnalytics
|
||||
|
||||
/**
|
||||
* F-Droid specific implementation of [org.meshtastic.analytics.platform.PlatformAnalytics]. This provides no-op
|
||||
* implementations for analytics and other platform services.
|
||||
* F-Droid specific implementation of [PlatformAnalytics]. This provides no-op implementations for analytics and other
|
||||
* platform services.
|
||||
*/
|
||||
class FdroidPlatformAnalytics @Inject constructor() : PlatformAnalytics {
|
||||
@Single
|
||||
class FdroidPlatformAnalytics : PlatformAnalytics {
|
||||
init {
|
||||
// For F-Droid builds we don't initialize external analytics services.
|
||||
// In debug builds we attach a DebugTree for convenient local logging, but
|
||||
// release builds rely on system logging only.
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.setMinSeverity(Severity.Debug)
|
||||
Logger.i { "F-Droid platform no-op analytics initialized (Debug mode }." }
|
||||
Logger.i { "F-Droid platform no-op analytics initialized (Debug mode)." }
|
||||
} else {
|
||||
Logger.setMinSeverity(Severity.Info)
|
||||
Logger.i { "F-Droid platform no-op analytics initialized." }
|
||||
|
|
@ -48,16 +47,6 @@ class FdroidPlatformAnalytics @Inject constructor() : PlatformAnalytics {
|
|||
Logger.d { "Set device attributes called: firmwareVersion=$firmwareVersion, deviceHardware=$model" }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun AddNavigationTrackingEffect(navController: NavHostController) {
|
||||
// No-op for F-Droid, but we can log navigation if needed for debugging
|
||||
if (BuildConfig.DEBUG) {
|
||||
navController.addOnDestinationChangedListener { _, destination, _ ->
|
||||
Logger.d { "Navigation changed to: ${destination.route}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val isPlatformServicesAvailable: Boolean
|
||||
get() = false
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* 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
|
||||
|
|
@ -14,40 +14,18 @@
|
|||
* 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 org.meshtastic.app.di
|
||||
|
||||
package org.meshtastic.core.network.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.koin.core.annotation.Module
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.model.NetworkDeviceHardware
|
||||
import org.meshtastic.core.model.NetworkFirmwareReleases
|
||||
import org.meshtastic.core.network.BuildConfig
|
||||
import org.meshtastic.core.network.service.ApiService
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
class FDroidNetworkModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder()
|
||||
.addInterceptor(
|
||||
interceptor =
|
||||
HttpLoggingInterceptor().apply {
|
||||
if (BuildConfig.DEBUG) {
|
||||
setLevel(HttpLoggingInterceptor.Level.BODY)
|
||||
}
|
||||
},
|
||||
)
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Single
|
||||
fun provideApiService(): ApiService = object : ApiService {
|
||||
override suspend fun getDeviceHardware(): List<NetworkDeviceHardware> =
|
||||
throw NotImplementedError("API calls to getDeviceHardware are not supported on Fdroid builds.")
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* 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
|
||||
|
|
@ -14,11 +14,9 @@
|
|||
* 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 org.meshtastic.app.di
|
||||
|
||||
package org.meshtastic.core.di
|
||||
import org.koin.core.annotation.Module
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class ProcessLifecycle
|
||||
@Module(includes = [FDroidNetworkModule::class])
|
||||
class FlavorModule
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* 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
|
||||
|
|
@ -14,8 +14,7 @@
|
|||
* 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 org.meshtastic.feature.intro
|
||||
package org.meshtastic.app.intro
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* 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 org.meshtastic.app.map
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.ui.util.MapViewProvider
|
||||
|
||||
/** OSMDroid implementation of [MapViewProvider]. */
|
||||
@Single
|
||||
class FdroidMapViewProvider : MapViewProvider {
|
||||
@Composable
|
||||
override fun MapView(modifier: Modifier, navigateToNodeDetails: (Int) -> Unit, waypointId: Int?) {
|
||||
val mapViewModel: MapViewModel = koinViewModel()
|
||||
LaunchedEffect(waypointId) { mapViewModel.setWaypointId(waypointId) }
|
||||
org.meshtastic.app.map.MapView(
|
||||
modifier = modifier,
|
||||
mapViewModel = mapViewModel,
|
||||
navigateToNodeDetails = navigateToNodeDetails,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* 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 org.meshtastic.app.map
|
||||
|
||||
import org.meshtastic.core.ui.util.MapViewProvider
|
||||
|
||||
fun getMapViewProvider(): MapViewProvider = FdroidMapViewProvider()
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* 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
|
||||
|
|
@ -14,8 +14,7 @@
|
|||
* 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 org.meshtastic.feature.map
|
||||
package org.meshtastic.app.map
|
||||
|
||||
import android.content.Context
|
||||
import android.util.TypedValue
|
||||
|
|
@ -14,13 +14,11 @@
|
|||
* 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 org.meshtastic.feature.map
|
||||
package org.meshtastic.app.map
|
||||
|
||||
import android.Manifest
|
||||
import android.graphics.Paint
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
|
|
@ -28,29 +26,21 @@ import androidx.compose.foundation.layout.Row
|
|||
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.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Layers
|
||||
import androidx.compose.material.icons.outlined.MyLocation
|
||||
import androidx.compose.material.icons.outlined.Tune
|
||||
import androidx.compose.material.icons.rounded.Check
|
||||
import androidx.compose.material.icons.rounded.Lens
|
||||
import androidx.compose.material.icons.rounded.LocationDisabled
|
||||
import androidx.compose.material.icons.rounded.PinDrop
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
|
|
@ -59,6 +49,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableDoubleStateOf
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -66,15 +57,12 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
|
|
@ -83,14 +71,20 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.meshtastic.app.R
|
||||
import org.meshtastic.app.map.cluster.RadiusMarkerClusterer
|
||||
import org.meshtastic.app.map.component.CacheLayout
|
||||
import org.meshtastic.app.map.component.DownloadButton
|
||||
import org.meshtastic.app.map.component.EditWaypointDialog
|
||||
import org.meshtastic.app.map.model.CustomTileSource
|
||||
import org.meshtastic.app.map.model.MarkerWithLabel
|
||||
import org.meshtastic.core.common.gpsDisabled
|
||||
import org.meshtastic.core.common.util.DateFormatter
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.database.entity.Packet
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.util.toString
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.calculating
|
||||
import org.meshtastic.core.resources.cancel
|
||||
|
|
@ -100,10 +94,8 @@ import org.meshtastic.core.resources.delete_for_everyone
|
|||
import org.meshtastic.core.resources.delete_for_me
|
||||
import org.meshtastic.core.resources.expires
|
||||
import org.meshtastic.core.resources.getString
|
||||
import org.meshtastic.core.resources.heading
|
||||
import org.meshtastic.core.resources.latitude
|
||||
import org.meshtastic.core.resources.last_heard_filter_label
|
||||
import org.meshtastic.core.resources.location_disabled
|
||||
import org.meshtastic.core.resources.longitude
|
||||
import org.meshtastic.core.resources.map_cache_info
|
||||
import org.meshtastic.core.resources.map_cache_manager
|
||||
import org.meshtastic.core.resources.map_cache_size
|
||||
|
|
@ -112,7 +104,6 @@ import org.meshtastic.core.resources.map_clear_tiles
|
|||
import org.meshtastic.core.resources.map_download_complete
|
||||
import org.meshtastic.core.resources.map_download_errors
|
||||
import org.meshtastic.core.resources.map_download_region
|
||||
import org.meshtastic.core.resources.map_filter
|
||||
import org.meshtastic.core.resources.map_node_popup_details
|
||||
import org.meshtastic.core.resources.map_offline_manager
|
||||
import org.meshtastic.core.resources.map_purge_fail
|
||||
|
|
@ -121,27 +112,25 @@ import org.meshtastic.core.resources.map_style_selection
|
|||
import org.meshtastic.core.resources.map_subDescription
|
||||
import org.meshtastic.core.resources.map_tile_source
|
||||
import org.meshtastic.core.resources.only_favorites
|
||||
import org.meshtastic.core.resources.position
|
||||
import org.meshtastic.core.resources.show_precision_circle
|
||||
import org.meshtastic.core.resources.show_waypoints
|
||||
import org.meshtastic.core.resources.toggle_my_position
|
||||
import org.meshtastic.core.resources.waypoint_delete
|
||||
import org.meshtastic.core.resources.you
|
||||
import org.meshtastic.core.ui.component.BasicListItem
|
||||
import org.meshtastic.core.ui.component.ListItem
|
||||
import org.meshtastic.core.ui.theme.TracerouteColors
|
||||
import org.meshtastic.core.ui.icon.Check
|
||||
import org.meshtastic.core.ui.icon.Favorite
|
||||
import org.meshtastic.core.ui.icon.Layers
|
||||
import org.meshtastic.core.ui.icon.Lens
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.icon.PinDrop
|
||||
import org.meshtastic.core.ui.util.formatAgo
|
||||
import org.meshtastic.core.ui.util.showToast
|
||||
import org.meshtastic.feature.map.cluster.RadiusMarkerClusterer
|
||||
import org.meshtastic.feature.map.component.CacheLayout
|
||||
import org.meshtastic.feature.map.component.DownloadButton
|
||||
import org.meshtastic.feature.map.component.EditWaypointDialog
|
||||
import org.meshtastic.feature.map.BaseMapViewModel.MapFilterState
|
||||
import org.meshtastic.feature.map.LastHeardFilter
|
||||
import org.meshtastic.feature.map.component.MapButton
|
||||
import org.meshtastic.feature.map.model.CustomTileSource
|
||||
import org.meshtastic.feature.map.model.MarkerWithLabel
|
||||
import org.meshtastic.feature.map.model.TracerouteOverlay
|
||||
import org.meshtastic.feature.map.component.MapControlsOverlay
|
||||
import org.meshtastic.proto.Config.DisplayConfig.DisplayUnits
|
||||
import org.meshtastic.proto.Position
|
||||
import org.meshtastic.proto.Waypoint
|
||||
import org.osmdroid.bonuspack.utils.BonusPackHelper.getBitmapFromVectorDrawable
|
||||
import org.osmdroid.config.Configuration
|
||||
|
|
@ -160,38 +149,23 @@ import org.osmdroid.views.MapView
|
|||
import org.osmdroid.views.overlay.MapEventsOverlay
|
||||
import org.osmdroid.views.overlay.Marker
|
||||
import org.osmdroid.views.overlay.Polygon
|
||||
import org.osmdroid.views.overlay.Polyline
|
||||
import org.osmdroid.views.overlay.infowindow.InfoWindow
|
||||
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
|
||||
import java.io.File
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.asin
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private fun MapView.updateMarkers(
|
||||
nodeMarkers: List<MarkerWithLabel>,
|
||||
waypointMarkers: List<MarkerWithLabel>,
|
||||
trackMarkers: List<Marker>,
|
||||
trackPolylines: List<Polyline>,
|
||||
nodeClusterer: RadiusMarkerClusterer,
|
||||
) {
|
||||
Logger.d {
|
||||
"Showing on map: ${nodeMarkers.size} nodes ${waypointMarkers.size} waypoints ${trackMarkers.size} tracks"
|
||||
}
|
||||
|
||||
val trackOverlayIds = (trackMarkers + trackPolylines).toSet()
|
||||
Logger.d { "Showing on map: ${nodeMarkers.size} nodes ${waypointMarkers.size} waypoints" }
|
||||
|
||||
overlays.removeAll { overlay ->
|
||||
overlay is MarkerWithLabel ||
|
||||
(overlay is Marker && overlay !in nodeClusterer.items && overlay !in trackOverlayIds) ||
|
||||
(overlay is Polyline && overlay !in trackOverlayIds)
|
||||
overlay is MarkerWithLabel || (overlay is Marker && overlay !in nodeClusterer.items)
|
||||
}
|
||||
|
||||
overlays.addAll(waypointMarkers)
|
||||
overlays.addAll(trackPolylines)
|
||||
overlays.addAll(trackMarkers)
|
||||
|
||||
nodeClusterer.items.clear()
|
||||
nodeClusterer.items.addAll(nodeMarkers)
|
||||
|
|
@ -229,16 +203,12 @@ private fun cacheManagerCallback(onTaskComplete: () -> Unit, onTaskFailed: (Int)
|
|||
* @param navigateToNodeDetails Callback to navigate to the details screen of a selected node.
|
||||
*/
|
||||
@OptIn(ExperimentalPermissionsApi::class) // Added for Accompanist
|
||||
@Suppress("CyclomaticComplexMethod", "LongParameterList", "LongMethod")
|
||||
@Suppress("CyclomaticComplexMethod", "LongMethod")
|
||||
@Composable
|
||||
fun MapView(
|
||||
mapViewModel: MapViewModel = hiltViewModel(),
|
||||
modifier: Modifier = Modifier,
|
||||
mapViewModel: MapViewModel = koinViewModel(),
|
||||
navigateToNodeDetails: (Int) -> Unit,
|
||||
focusedNodeNum: Int? = null,
|
||||
nodeTracks: List<Position>? = null,
|
||||
tracerouteOverlay: TracerouteOverlay? = null,
|
||||
tracerouteNodePositions: Map<Int, Position> = emptyMap(),
|
||||
onTracerouteMappableCountChanged: (shown: Int, total: Int) -> Unit = { _, _ -> },
|
||||
) {
|
||||
var mapFilterExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
|
|
@ -337,6 +307,16 @@ fun MapView(
|
|||
}
|
||||
}
|
||||
|
||||
// Keep screen on while location tracking is active
|
||||
LaunchedEffect(myLocationOverlay) {
|
||||
val activity = context as? android.app.Activity ?: return@LaunchedEffect
|
||||
if (myLocationOverlay != null) {
|
||||
activity.window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
} else {
|
||||
activity.window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
}
|
||||
|
||||
val nodes by mapViewModel.nodes.collectAsStateWithLifecycle()
|
||||
val waypoints by mapViewModel.waypoints.collectAsStateWithLifecycle(emptyMap())
|
||||
val selectedWaypointId by mapViewModel.selectedWaypointId.collectAsStateWithLifecycle()
|
||||
|
|
@ -344,7 +324,7 @@ fun MapView(
|
|||
|
||||
LaunchedEffect(selectedWaypointId, waypoints) {
|
||||
if (selectedWaypointId != null && waypoints.containsKey(selectedWaypointId)) {
|
||||
waypoints[selectedWaypointId]?.data?.waypoint?.let { pt ->
|
||||
waypoints[selectedWaypointId]?.waypoint?.let { pt ->
|
||||
val geoPoint = GeoPoint((pt.latitude_i ?: 0) * 1e-7, (pt.longitude_i ?: 0) * 1e-7)
|
||||
map.controller.setCenter(geoPoint)
|
||||
map.controller.setZoom(WAYPOINT_ZOOM)
|
||||
|
|
@ -352,77 +332,21 @@ fun MapView(
|
|||
}
|
||||
}
|
||||
|
||||
val tracerouteSelection =
|
||||
remember(tracerouteOverlay, tracerouteNodePositions, nodes) {
|
||||
mapViewModel.tracerouteNodeSelection(
|
||||
tracerouteOverlay = tracerouteOverlay,
|
||||
tracerouteNodePositions = tracerouteNodePositions,
|
||||
nodes = nodes,
|
||||
)
|
||||
}
|
||||
val overlayNodeNums = tracerouteSelection.overlayNodeNums
|
||||
val nodeLookup = tracerouteSelection.nodeLookup
|
||||
val nodesForMarkers = tracerouteSelection.nodesForMarkers
|
||||
val tracerouteForwardPoints =
|
||||
remember(tracerouteOverlay, nodeLookup) {
|
||||
tracerouteOverlay?.forwardRoute?.mapNotNull {
|
||||
nodeLookup[it]?.let { node -> GeoPoint(node.latitude, node.longitude) }
|
||||
} ?: emptyList()
|
||||
}
|
||||
val tracerouteReturnPoints =
|
||||
remember(tracerouteOverlay, nodeLookup) {
|
||||
tracerouteOverlay?.returnRoute?.mapNotNull {
|
||||
nodeLookup[it]?.let { node -> GeoPoint(node.latitude, node.longitude) }
|
||||
} ?: emptyList()
|
||||
}
|
||||
LaunchedEffect(tracerouteOverlay, nodesForMarkers) {
|
||||
if (tracerouteOverlay != null) {
|
||||
onTracerouteMappableCountChanged(nodesForMarkers.size, tracerouteOverlay.relatedNodeNums.size)
|
||||
}
|
||||
}
|
||||
val tracerouteHeadingReferencePoints =
|
||||
remember(tracerouteForwardPoints, tracerouteReturnPoints) {
|
||||
when {
|
||||
tracerouteForwardPoints.size >= 2 -> tracerouteForwardPoints
|
||||
tracerouteReturnPoints.size >= 2 -> tracerouteReturnPoints
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
val tracerouteForwardOffsetPoints =
|
||||
remember(tracerouteForwardPoints, tracerouteHeadingReferencePoints) {
|
||||
offsetPolyline(
|
||||
points = tracerouteForwardPoints,
|
||||
offsetMeters = TRACEROUTE_OFFSET_METERS,
|
||||
headingReferencePoints = tracerouteHeadingReferencePoints,
|
||||
sideMultiplier = 1.0,
|
||||
)
|
||||
}
|
||||
val tracerouteReturnOffsetPoints =
|
||||
remember(tracerouteReturnPoints, tracerouteHeadingReferencePoints) {
|
||||
offsetPolyline(
|
||||
points = tracerouteReturnPoints,
|
||||
offsetMeters = TRACEROUTE_OFFSET_METERS,
|
||||
headingReferencePoints = tracerouteHeadingReferencePoints,
|
||||
sideMultiplier = -1.0,
|
||||
)
|
||||
}
|
||||
val traceroutePolylines = remember { mutableStateListOf<Polyline>() }
|
||||
var hasCenteredTraceroute by remember(tracerouteOverlay) { mutableStateOf(false) }
|
||||
|
||||
val markerIcon = remember { AppCompatResources.getDrawable(context, R.drawable.ic_location_on) }
|
||||
|
||||
fun MapView.onNodesChanged(nodes: Collection<Node>): List<MarkerWithLabel> {
|
||||
val nodesWithPosition = nodes.filter { it.validPosition != null }
|
||||
val ourNode = mapViewModel.ourNodeInfo.value
|
||||
val displayUnits =
|
||||
mapViewModel.config.display?.units ?: org.meshtastic.proto.Config.DisplayConfig.DisplayUnits.METRIC
|
||||
val displayUnits = mapViewModel.config.display?.units ?: DisplayUnits.METRIC
|
||||
val mapFilterStateValue = mapViewModel.mapFilterStateFlow.value // Access mapFilterState directly
|
||||
return nodesWithPosition.mapNotNull { node ->
|
||||
if (mapFilterStateValue.onlyFavorites && !node.isFavorite && !node.equals(ourNode)) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
if (
|
||||
mapFilterStateValue.onlyFavorites &&
|
||||
!node.isFavorite &&
|
||||
!overlayNodeNums.contains(node.num) &&
|
||||
!node.equals(ourNode)
|
||||
mapFilterStateValue.lastHeardFilter.seconds != 0L &&
|
||||
(nowSeconds - node.lastHeard) > mapFilterStateValue.lastHeardFilter.seconds &&
|
||||
node.num != ourNode?.num
|
||||
) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
|
@ -441,7 +365,9 @@ fun MapView(
|
|||
if (node.batteryStr != "") node.batteryStr else "?",
|
||||
)
|
||||
ourNode?.distanceStr(node, displayUnits)?.let { dist ->
|
||||
subDescription = getString(Res.string.map_subDescription, ourNode.bearing(node).toString(), dist)
|
||||
ourNode.bearing(node)?.let { bearing ->
|
||||
subDescription = getString(Res.string.map_subDescription, bearing, dist)
|
||||
}
|
||||
}
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||
position = nodePosition
|
||||
|
|
@ -450,7 +376,7 @@ fun MapView(
|
|||
if (!mapFilterStateValue.showPrecisionCircle) {
|
||||
setPrecisionBits(0)
|
||||
} else {
|
||||
setPrecisionBits(p.precision_bits ?: 0)
|
||||
setPrecisionBits(p.precision_bits)
|
||||
}
|
||||
setOnLongClickListener {
|
||||
navigateToNodeDetails(node.num)
|
||||
|
|
@ -470,7 +396,7 @@ fun MapView(
|
|||
Logger.d { "User deleted waypoint ${waypoint.id} for me" }
|
||||
mapViewModel.deleteWaypoint(waypoint.id)
|
||||
}
|
||||
if ((waypoint.locked_to ?: 0) in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) {
|
||||
if (waypoint.locked_to in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) {
|
||||
builder.setPositiveButton(getString(Res.string.delete_for_everyone)) { _, _ ->
|
||||
Logger.d { "User deleted waypoint ${waypoint.id} for everyone" }
|
||||
mapViewModel.sendWaypoint(waypoint.copy(expire = 1))
|
||||
|
|
@ -496,9 +422,9 @@ fun MapView(
|
|||
fun showMarkerLongPressDialog(id: Int) {
|
||||
performHapticFeedback()
|
||||
Logger.d { "marker long pressed id=$id" }
|
||||
val waypoint = waypoints[id]?.data?.waypoint ?: return
|
||||
val waypoint = waypoints[id]?.waypoint ?: return
|
||||
// edit only when unlocked or lockedTo myNodeNum
|
||||
if ((waypoint.locked_to ?: 0) in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) {
|
||||
if (waypoint.locked_to in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) {
|
||||
showEditWaypointDialog = waypoint
|
||||
} else {
|
||||
showDeleteMarkerDialog(waypoint)
|
||||
|
|
@ -512,25 +438,25 @@ fun MapView(
|
|||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun MapView.onWaypointChanged(waypoints: Collection<Packet>, selectedWaypointId: Int?): List<MarkerWithLabel> {
|
||||
fun MapView.onWaypointChanged(waypoints: Collection<DataPacket>, selectedWaypointId: Int?): List<MarkerWithLabel> {
|
||||
return waypoints.mapNotNull { waypoint ->
|
||||
val pt = waypoint.data.waypoint ?: return@mapNotNull null
|
||||
val pt = waypoint.waypoint ?: return@mapNotNull null
|
||||
if (!mapFilterState.showWaypoints) return@mapNotNull null // Use collected mapFilterState
|
||||
val lock = if ((pt.locked_to ?: 0) != 0) "\uD83D\uDD12" else ""
|
||||
val time = DateFormatter.formatDateTime(waypoint.received_time)
|
||||
val label = (pt.name ?: "") + " " + formatAgo((waypoint.received_time / 1000).toInt())
|
||||
val emoji = String(Character.toChars(if ((pt.icon ?: 0) == 0) 128205 else pt.icon!!))
|
||||
val lock = if (pt.locked_to != 0) "\uD83D\uDD12" else ""
|
||||
val time = DateFormatter.formatDateTime(waypoint.time)
|
||||
val label = pt.name + " " + formatAgo((waypoint.time / 1000).toInt())
|
||||
val emoji = String(Character.toChars(if (pt.icon == 0) 128205 else pt.icon))
|
||||
val now = nowMillis
|
||||
val expireTimeMillis = (pt.expire ?: 0) * 1000L
|
||||
val expireTimeMillis = pt.expire * 1000L
|
||||
val expireTimeStr =
|
||||
when {
|
||||
(pt.expire ?: 0) == 0 || pt.expire == Int.MAX_VALUE -> "Never"
|
||||
pt.expire == 0 || pt.expire == Int.MAX_VALUE -> "Never"
|
||||
expireTimeMillis <= now -> "Expired"
|
||||
else -> DateFormatter.formatRelativeTime(expireTimeMillis)
|
||||
}
|
||||
MarkerWithLabel(this, label, emoji).apply {
|
||||
id = "${pt.id}"
|
||||
title = "${pt.name} (${getUsername(waypoint.data.from)}$lock)"
|
||||
title = "${pt.name} (${getUsername(waypoint.from)}$lock)"
|
||||
snippet = "[$time] ${pt.description} " + getString(Res.string.expires) + ": $expireTimeStr"
|
||||
position = GeoPoint((pt.latitude_i ?: 0) * 1e-7, (pt.longitude_i ?: 0) * 1e-7)
|
||||
if (selectedWaypointId == pt.id) {
|
||||
|
|
@ -581,53 +507,6 @@ fun MapView(
|
|||
invalidate()
|
||||
}
|
||||
|
||||
fun MapView.updateTracerouteOverlay(forwardPoints: List<GeoPoint>, returnPoints: List<GeoPoint>) {
|
||||
overlays.removeAll(traceroutePolylines)
|
||||
traceroutePolylines.clear()
|
||||
|
||||
fun buildPolyline(points: List<GeoPoint>, color: Int, strokeWidth: Float): Polyline = Polyline().apply {
|
||||
setPoints(points)
|
||||
outlinePaint.apply {
|
||||
this.color = color
|
||||
this.strokeWidth = strokeWidth
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
style = Paint.Style.STROKE
|
||||
}
|
||||
}
|
||||
|
||||
forwardPoints
|
||||
.takeIf { it.size >= 2 }
|
||||
?.let { points ->
|
||||
traceroutePolylines.add(
|
||||
buildPolyline(points, TracerouteColors.OutgoingRoute.toArgb(), with(density) { 6.dp.toPx() }),
|
||||
)
|
||||
}
|
||||
returnPoints
|
||||
.takeIf { it.size >= 2 }
|
||||
?.let { points ->
|
||||
traceroutePolylines.add(
|
||||
buildPolyline(points, TracerouteColors.ReturnRoute.toArgb(), with(density) { 5.dp.toPx() }),
|
||||
)
|
||||
}
|
||||
overlays.addAll(traceroutePolylines)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
LaunchedEffect(tracerouteOverlay, tracerouteForwardPoints, tracerouteReturnPoints) {
|
||||
if (tracerouteOverlay == null || hasCenteredTraceroute) return@LaunchedEffect
|
||||
val allPoints = (tracerouteForwardPoints + tracerouteReturnPoints).distinct()
|
||||
if (allPoints.isNotEmpty()) {
|
||||
if (allPoints.size == 1) {
|
||||
map.controller.setCenter(allPoints.first())
|
||||
map.controller.setZoom(TRACEROUTE_SINGLE_POINT_ZOOM)
|
||||
} else {
|
||||
map.zoomToBoundingBox(BoundingBox.fromGeoPoints(allPoints).zoomIn(-TRACEROUTE_ZOOM_OUT_LEVELS), true)
|
||||
}
|
||||
hasCenteredTraceroute = true
|
||||
}
|
||||
}
|
||||
|
||||
fun MapView.generateBoxOverlay() {
|
||||
overlays.removeAll { it is Polygon }
|
||||
val zoomFactor = 1.3
|
||||
|
|
@ -690,52 +569,8 @@ fun MapView(
|
|||
}
|
||||
}
|
||||
|
||||
fun MapView.onTracksChanged(nodeTracks: List<Position>?, focusedNodeNum: Int?): Pair<List<Marker>, List<Polyline>> {
|
||||
if (nodeTracks == null || focusedNodeNum == null) return emptyList<Marker>() to emptyList<Polyline>()
|
||||
|
||||
val lastHeardTrackFilter = mapFilterState.lastHeardTrackFilter
|
||||
val timeFilteredPositions =
|
||||
nodeTracks.filter {
|
||||
lastHeardTrackFilter == LastHeardFilter.Any || it.time > nowSeconds - lastHeardTrackFilter.seconds
|
||||
}
|
||||
val sortedPositions = timeFilteredPositions.sortedBy { it.time }
|
||||
|
||||
val focusedNode = nodes.find { it.num == focusedNodeNum } ?: return emptyList<Marker>() to emptyList<Polyline>()
|
||||
val color = focusedNode.colors.second
|
||||
|
||||
val trackPolylines = mutableListOf<Polyline>()
|
||||
if (sortedPositions.size > 1) {
|
||||
val segments = sortedPositions.windowed(size = 2, step = 1, partialWindows = false)
|
||||
segments.forEachIndexed { index, segmentPoints ->
|
||||
val alpha = (index.toFloat() / (segments.size.toFloat() - 1))
|
||||
val polyline =
|
||||
Polyline().apply {
|
||||
setPoints(
|
||||
segmentPoints.map { GeoPoint((it.latitude_i ?: 0) * 1e-7, (it.longitude_i ?: 0) * 1e-7) },
|
||||
)
|
||||
outlinePaint.color = Color(color).copy(alpha = alpha).toArgb()
|
||||
outlinePaint.strokeWidth = 8f
|
||||
}
|
||||
trackPolylines.add(polyline)
|
||||
}
|
||||
}
|
||||
|
||||
val trackMarkers =
|
||||
sortedPositions.mapIndexedNotNull { index, position ->
|
||||
if (index == sortedPositions.lastIndex) return@mapIndexedNotNull null
|
||||
|
||||
Marker(this).apply {
|
||||
this.position = GeoPoint((position.latitude_i ?: 0) * 1e-7, (position.longitude_i ?: 0) * 1e-7)
|
||||
icon = AppCompatResources.getDrawable(context, R.drawable.ic_map_location_dot)
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
|
||||
title = getString(Res.string.position)
|
||||
snippet = formatAgo(position.time)
|
||||
}
|
||||
}
|
||||
return trackMarkers to trackPolylines
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
floatingActionButton = {
|
||||
DownloadButton(showDownloadButton && downloadRegionBoundingBox == null) { showCacheManagerDialog = true }
|
||||
},
|
||||
|
|
@ -750,14 +585,10 @@ fun MapView(
|
|||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
update = { mapView ->
|
||||
mapView.updateTracerouteOverlay(tracerouteForwardOffsetPoints, tracerouteReturnOffsetPoints)
|
||||
val (trackMarkers, trackPolylines) = mapView.onTracksChanged(nodeTracks, focusedNodeNum)
|
||||
with(mapView) {
|
||||
updateMarkers(
|
||||
onNodesChanged(nodesForMarkers),
|
||||
onNodesChanged(nodes),
|
||||
onWaypointChanged(waypoints.values, selectedWaypointId),
|
||||
trackMarkers,
|
||||
trackPolylines,
|
||||
nodeClusterer,
|
||||
)
|
||||
}
|
||||
|
|
@ -776,122 +607,34 @@ fun MapView(
|
|||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
)
|
||||
} else {
|
||||
@Suppress("MagicNumber")
|
||||
Column(
|
||||
modifier = Modifier.padding(top = 16.dp, end = 16.dp).align(Alignment.TopEnd),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
MapButton(
|
||||
onClick = { showMapStyleDialog = true },
|
||||
icon = Icons.Outlined.Layers,
|
||||
contentDescription = Res.string.map_style_selection,
|
||||
)
|
||||
Box(modifier = Modifier) {
|
||||
MapButton(
|
||||
onClick = { mapFilterExpanded = true },
|
||||
icon = Icons.Outlined.Tune,
|
||||
contentDescription = stringResource(Res.string.map_filter),
|
||||
)
|
||||
DropdownMenu(
|
||||
MapControlsOverlay(
|
||||
modifier = Modifier.align(Alignment.TopCenter).padding(top = 8.dp),
|
||||
onToggleFilterMenu = { mapFilterExpanded = true },
|
||||
filterDropdownContent = {
|
||||
FdroidMainMapFilterDropdown(
|
||||
expanded = mapFilterExpanded,
|
||||
onDismissRequest = { mapFilterExpanded = false },
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Star,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.only_favorites),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Checkbox(
|
||||
checked = mapFilterState.onlyFavorites,
|
||||
onCheckedChange = { mapViewModel.toggleOnlyFavorites() },
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = { mapViewModel.toggleOnlyFavorites() },
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.PinDrop,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.show_waypoints),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Checkbox(
|
||||
checked = mapFilterState.showWaypoints,
|
||||
onCheckedChange = { mapViewModel.toggleShowWaypointsOnMap() },
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = { mapViewModel.toggleShowWaypointsOnMap() },
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Lens,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.show_precision_circle),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
@Suppress("MagicNumber")
|
||||
Checkbox(
|
||||
checked = mapFilterState.showPrecisionCircle,
|
||||
onCheckedChange = { mapViewModel.toggleShowPrecisionCircleOnMap() },
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = { mapViewModel.toggleShowPrecisionCircleOnMap() },
|
||||
)
|
||||
}
|
||||
}
|
||||
MapButton(
|
||||
icon =
|
||||
if (myLocationOverlay == null) {
|
||||
Icons.Outlined.MyLocation
|
||||
} else {
|
||||
Icons.Rounded.LocationDisabled
|
||||
},
|
||||
contentDescription = stringResource(Res.string.toggle_my_position),
|
||||
) {
|
||||
mapFilterState = mapFilterState,
|
||||
mapViewModel = mapViewModel,
|
||||
)
|
||||
},
|
||||
mapTypeContent = {
|
||||
MapButton(
|
||||
icon = MeshtasticIcons.Layers,
|
||||
contentDescription = stringResource(Res.string.map_style_selection),
|
||||
onClick = { showMapStyleDialog = true },
|
||||
)
|
||||
},
|
||||
isLocationTrackingEnabled = myLocationOverlay != null,
|
||||
onToggleLocationTracking = {
|
||||
if (locationPermissionsState.allPermissionsGranted) {
|
||||
map.toggleMyLocation()
|
||||
} else {
|
||||
triggerLocationToggleAfterPermission = true
|
||||
locationPermissionsState.launchMultiplePermissionRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -941,12 +684,11 @@ fun MapView(
|
|||
Logger.d { "User clicked send waypoint ${waypoint.id}" }
|
||||
showEditWaypointDialog = null
|
||||
|
||||
val newId =
|
||||
if (waypoint.id == 0) mapViewModel.generatePacketId() ?: return@EditWaypointDialog else waypoint.id
|
||||
val newId = if (waypoint.id == 0) mapViewModel.generatePacketId() else waypoint.id
|
||||
val newName = if (waypoint.name.isNullOrEmpty()) "Dropped Pin" else waypoint.name
|
||||
val newExpire = if ((waypoint.expire ?: 0) == 0) Int.MAX_VALUE else (waypoint.expire ?: Int.MAX_VALUE)
|
||||
val newLockedTo = if ((waypoint.locked_to ?: 0) != 0) mapViewModel.myNodeNum ?: 0 else 0
|
||||
val newIcon = if ((waypoint.icon ?: 0) == 0) 128205 else waypoint.icon
|
||||
val newExpire = if (waypoint.expire == 0) Int.MAX_VALUE else waypoint.expire
|
||||
val newLockedTo = if (waypoint.locked_to != 0) mapViewModel.myNodeNum ?: 0 else 0
|
||||
val newIcon = if (waypoint.icon == 0) 128205 else waypoint.icon
|
||||
|
||||
mapViewModel.sendWaypoint(
|
||||
waypoint.copy(
|
||||
|
|
@ -971,6 +713,103 @@ fun MapView(
|
|||
}
|
||||
}
|
||||
|
||||
/** F-Droid main map filter dropdown — favorites, waypoints, precision circle, and last-heard time filter slider. */
|
||||
@Composable
|
||||
private fun FdroidMainMapFilterDropdown(
|
||||
expanded: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
mapFilterState: MapFilterState,
|
||||
mapViewModel: MapViewModel,
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = MeshtasticIcons.Favorite,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Text(text = stringResource(Res.string.only_favorites), modifier = Modifier.weight(1f))
|
||||
Checkbox(
|
||||
checked = mapFilterState.onlyFavorites,
|
||||
onCheckedChange = { mapViewModel.toggleOnlyFavorites() },
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = { mapViewModel.toggleOnlyFavorites() },
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = MeshtasticIcons.PinDrop,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Text(text = stringResource(Res.string.show_waypoints), modifier = Modifier.weight(1f))
|
||||
Checkbox(
|
||||
checked = mapFilterState.showWaypoints,
|
||||
onCheckedChange = { mapViewModel.toggleShowWaypointsOnMap() },
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = { mapViewModel.toggleShowWaypointsOnMap() },
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = MeshtasticIcons.Lens,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Text(text = stringResource(Res.string.show_precision_circle), modifier = Modifier.weight(1f))
|
||||
Checkbox(
|
||||
checked = mapFilterState.showPrecisionCircle,
|
||||
onCheckedChange = { mapViewModel.toggleShowPrecisionCircleOnMap() },
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = { mapViewModel.toggleShowPrecisionCircleOnMap() },
|
||||
)
|
||||
HorizontalDivider()
|
||||
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
|
||||
val filterOptions = LastHeardFilter.entries
|
||||
val selectedIndex = filterOptions.indexOf(mapFilterState.lastHeardFilter)
|
||||
var sliderPosition by remember(selectedIndex) { mutableFloatStateOf(selectedIndex.toFloat()) }
|
||||
Text(
|
||||
text =
|
||||
stringResource(
|
||||
Res.string.last_heard_filter_label,
|
||||
stringResource(mapFilterState.lastHeardFilter.label),
|
||||
),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
Slider(
|
||||
value = sliderPosition,
|
||||
onValueChange = { sliderPosition = it },
|
||||
onValueChangeFinished = {
|
||||
val newIndex = sliderPosition.roundToInt().coerceIn(0, filterOptions.size - 1)
|
||||
mapViewModel.setLastHeardFilter(filterOptions[newIndex])
|
||||
},
|
||||
valueRange = 0f..(filterOptions.size - 1).toFloat(),
|
||||
steps = filterOptions.size - 2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MapStyleDialog(selectedMapStyle: Int, onDismiss: () -> Unit, onSelectMapStyle: (Int) -> Unit) {
|
||||
val selected = remember { mutableStateOf(selectedMapStyle) }
|
||||
|
|
@ -979,7 +818,7 @@ private fun MapStyleDialog(selectedMapStyle: Int, onDismiss: () -> Unit, onSelec
|
|||
CustomTileSource.mTileSources.values.forEachIndexed { index, style ->
|
||||
ListItem(
|
||||
text = style,
|
||||
trailingIcon = if (index == selected.value) Icons.Rounded.Check else null,
|
||||
trailingIcon = if (index == selected.value) MeshtasticIcons.Check else null,
|
||||
onClick = {
|
||||
selected.value = index
|
||||
onSelectMapStyle(index)
|
||||
|
|
@ -1022,15 +861,9 @@ private fun CacheInfoDialog(mapView: MapView, onDismiss: () -> Unit) {
|
|||
onDismiss = onDismiss,
|
||||
negativeButton = { TextButton(onClick = { onDismiss() }) { Text(text = stringResource(Res.string.close)) } },
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
text =
|
||||
stringResource(
|
||||
Res.string.map_cache_info,
|
||||
cacheCapacity / (1024.0 * 1024.0),
|
||||
currentCacheUsage / (1024.0 * 1024.0),
|
||||
),
|
||||
)
|
||||
val capacityMb = (cacheCapacity / (1024 * 1024)).toLong()
|
||||
val usageMb = (currentCacheUsage / (1024 * 1024)).toLong()
|
||||
Text(modifier = Modifier.padding(16.dp), text = stringResource(Res.string.map_cache_info, capacityMb, usageMb))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1126,57 +959,4 @@ private fun MapsDialog(
|
|||
}
|
||||
}
|
||||
|
||||
private const val EARTH_RADIUS_METERS = 6_371_000.0
|
||||
private const val TRACEROUTE_OFFSET_METERS = 100.0
|
||||
private const val TRACEROUTE_SINGLE_POINT_ZOOM = 12.0
|
||||
private const val TRACEROUTE_ZOOM_OUT_LEVELS = 0.5
|
||||
private const val WAYPOINT_ZOOM = 15.0
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun Double.toRad(): Double = this * Math.PI / 180.0
|
||||
|
||||
private fun bearingRad(from: GeoPoint, to: GeoPoint): Double {
|
||||
val lat1 = from.latitude.toRad()
|
||||
val lat2 = to.latitude.toRad()
|
||||
val dLon = (to.longitude - from.longitude).toRad()
|
||||
return atan2(sin(dLon) * cos(lat2), cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon))
|
||||
}
|
||||
|
||||
private fun GeoPoint.offsetPoint(headingRad: Double, offsetMeters: Double): GeoPoint {
|
||||
val distanceByRadius = offsetMeters / EARTH_RADIUS_METERS
|
||||
val lat1 = latitude.toRad()
|
||||
val lon1 = longitude.toRad()
|
||||
val lat2 = asin(sin(lat1) * cos(distanceByRadius) + cos(lat1) * sin(distanceByRadius) * cos(headingRad))
|
||||
val lon2 =
|
||||
lon1 + atan2(sin(headingRad) * sin(distanceByRadius) * cos(lat1), cos(distanceByRadius) - sin(lat1) * sin(lat2))
|
||||
return GeoPoint(Math.toDegrees(lat2), Math.toDegrees(lon2))
|
||||
}
|
||||
|
||||
private fun offsetPolyline(
|
||||
points: List<GeoPoint>,
|
||||
offsetMeters: Double,
|
||||
headingReferencePoints: List<GeoPoint> = points,
|
||||
sideMultiplier: Double = 1.0,
|
||||
): List<GeoPoint> {
|
||||
val headingPoints = headingReferencePoints.takeIf { it.size >= 2 } ?: points
|
||||
if (points.size < 2 || headingPoints.size < 2 || offsetMeters == 0.0) return points
|
||||
|
||||
val headings =
|
||||
headingPoints.mapIndexed { index, _ ->
|
||||
when (index) {
|
||||
0 -> bearingRad(headingPoints[0], headingPoints[1])
|
||||
headingPoints.lastIndex ->
|
||||
bearingRad(headingPoints[headingPoints.lastIndex - 1], headingPoints[headingPoints.lastIndex])
|
||||
|
||||
else -> bearingRad(headingPoints[index - 1], headingPoints[index + 1])
|
||||
}
|
||||
}
|
||||
|
||||
return points.mapIndexed { index, point ->
|
||||
val heading = headings[index.coerceIn(0, headings.lastIndex)]
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
val perpendicularHeading = heading + (Math.PI / 2 * sideMultiplier)
|
||||
point.offsetPoint(perpendicularHeading, abs(offsetMeters))
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* 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 org.meshtastic.feature.map
|
||||
package org.meshtastic.app.map
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.DashPathEffect
|
||||
|
|
@ -24,6 +24,7 @@ import androidx.compose.ui.unit.Density
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.meshtastic.app.R
|
||||
import org.meshtastic.proto.Position
|
||||
import org.osmdroid.util.GeoPoint
|
||||
import org.osmdroid.views.MapView
|
||||
|
|
@ -123,17 +124,17 @@ fun MapView.addPolyline(density: Density, geoPoints: List<GeoPoint>, onClick: ()
|
|||
return polyline
|
||||
}
|
||||
|
||||
fun MapView.addPositionMarkers(positions: List<Position>, onClick: () -> Unit): List<Marker> {
|
||||
fun MapView.addPositionMarkers(positions: List<Position>, onClick: (Int) -> Unit): List<Marker> {
|
||||
val navIcon = ContextCompat.getDrawable(context, R.drawable.ic_map_navigation)
|
||||
val markers =
|
||||
positions.map {
|
||||
positions.map { pos ->
|
||||
Marker(this).apply {
|
||||
icon = navIcon
|
||||
rotation = ((it.ground_track ?: 0) * 1e-5).toFloat()
|
||||
rotation = ((pos.ground_track ?: 0) * 1e-5).toFloat()
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
|
||||
position = GeoPoint((it.latitude_i ?: 0) * 1e-7, (it.longitude_i ?: 0) * 1e-7)
|
||||
position = GeoPoint((pos.latitude_i ?: 0) * 1e-7, (pos.longitude_i ?: 0) * 1e-7)
|
||||
setOnMarkerClickListener { _, _ ->
|
||||
onClick()
|
||||
onClick(pos.time)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
@ -14,47 +14,48 @@
|
|||
* 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 org.meshtastic.feature.map
|
||||
package org.meshtastic.app.map
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.navigation.toRoute
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.koin.core.annotation.KoinViewModel
|
||||
import org.meshtastic.core.common.BuildConfigProvider
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.navigation.MapRoutes
|
||||
import org.meshtastic.core.prefs.map.MapPrefs
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.repository.MapPrefs
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.feature.map.BaseMapViewModel
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@HiltViewModel
|
||||
class MapViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
@KoinViewModel
|
||||
class MapViewModel(
|
||||
mapPrefs: MapPrefs,
|
||||
packetRepository: PacketRepository,
|
||||
private val nodeRepository: NodeRepository,
|
||||
serviceRepository: ServiceRepository,
|
||||
nodeRepository: NodeRepository,
|
||||
radioController: RadioController,
|
||||
radioConfigRepository: RadioConfigRepository,
|
||||
buildConfigProvider: BuildConfigProvider,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, serviceRepository) {
|
||||
) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, radioController) {
|
||||
|
||||
private val _selectedWaypointId = MutableStateFlow(savedStateHandle.toRoute<MapRoutes.Map>().waypointId)
|
||||
private val _selectedWaypointId = MutableStateFlow(savedStateHandle.get<Int>("waypointId"))
|
||||
val selectedWaypointId: StateFlow<Int?> = _selectedWaypointId.asStateFlow()
|
||||
|
||||
fun setWaypointId(id: Int?) {
|
||||
if (_selectedWaypointId.value != id) {
|
||||
_selectedWaypointId.value = id
|
||||
}
|
||||
}
|
||||
|
||||
var mapStyleId: Int
|
||||
get() = mapPrefs.mapStyle
|
||||
get() = mapPrefs.mapStyle.value
|
||||
set(value) {
|
||||
mapPrefs.mapStyle = value
|
||||
mapPrefs.setMapStyle(value)
|
||||
}
|
||||
|
||||
val localConfig = radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig())
|
||||
|
|
@ -63,6 +64,4 @@ constructor(
|
|||
get() = localConfig.value
|
||||
|
||||
val applicationId = buildConfigProvider.applicationId
|
||||
|
||||
override fun getUser(userId: String?) = nodeRepository.getUser(userId ?: DataPacket.ID_BROADCAST)
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* 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
|
||||
|
|
@ -14,12 +14,8 @@
|
|||
* 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 org.meshtastic.app.map
|
||||
|
||||
package org.meshtastic.feature.map
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.PowerManager
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -33,7 +29,6 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.osmdroid.config.Configuration
|
||||
import org.osmdroid.tileprovider.tilesource.ITileSource
|
||||
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
|
||||
|
|
@ -42,29 +37,6 @@ import org.osmdroid.util.GeoPoint
|
|||
import org.osmdroid.views.CustomZoomButtonsController
|
||||
import org.osmdroid.views.MapView
|
||||
|
||||
@SuppressLint("WakelockTimeout")
|
||||
private fun PowerManager.WakeLock.safeAcquire() {
|
||||
if (!isHeld) {
|
||||
try {
|
||||
acquire()
|
||||
} catch (e: SecurityException) {
|
||||
Logger.e { "WakeLock permission exception: ${e.message}" }
|
||||
} catch (e: IllegalStateException) {
|
||||
Logger.e { "WakeLock acquire() exception: ${e.message}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun PowerManager.WakeLock.safeRelease() {
|
||||
if (isHeld) {
|
||||
try {
|
||||
release()
|
||||
} catch (e: IllegalStateException) {
|
||||
Logger.e { "WakeLock release() exception: ${e.message}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val MIN_ZOOM_LEVEL = 1.5
|
||||
private const val MAX_ZOOM_LEVEL = 20.0
|
||||
private const val DEFAULT_ZOOM_LEVEL = 15.0
|
||||
|
|
@ -137,22 +109,13 @@ internal fun rememberMapViewWithLifecycle(
|
|||
}
|
||||
val lifecycle = LocalLifecycleOwner.current.lifecycle
|
||||
DisposableEffect(lifecycle) {
|
||||
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val wakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "Meshtastic:MapViewLock")
|
||||
|
||||
wakeLock.safeAcquire()
|
||||
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_PAUSE -> {
|
||||
wakeLock.safeRelease()
|
||||
mapView.onPause()
|
||||
}
|
||||
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
wakeLock.safeAcquire()
|
||||
mapView.onResume()
|
||||
}
|
||||
|
||||
|
|
@ -167,10 +130,7 @@ internal fun rememberMapViewWithLifecycle(
|
|||
|
||||
lifecycle.addObserver(observer)
|
||||
|
||||
onDispose {
|
||||
lifecycle.removeObserver(observer)
|
||||
wakeLock.safeRelease()
|
||||
}
|
||||
onDispose { lifecycle.removeObserver(observer) }
|
||||
}
|
||||
return mapView
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* 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 org.meshtastic.feature.map
|
||||
package org.meshtastic.app.map
|
||||
|
||||
import android.database.Cursor
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* 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 org.meshtastic.feature.map.component
|
||||
package org.meshtastic.app.map.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -14,15 +14,13 @@
|
|||
* 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 org.meshtastic.feature.map.component
|
||||
package org.meshtastic.app.map.component
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Download
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -32,6 +30,8 @@ import androidx.compose.ui.draw.scale
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.map_download_region
|
||||
import org.meshtastic.core.ui.icon.Download
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
|
||||
@Composable
|
||||
fun DownloadButton(enabled: Boolean, onClick: () -> Unit) {
|
||||
|
|
@ -50,7 +50,7 @@ fun DownloadButton(enabled: Boolean, onClick: () -> Unit) {
|
|||
) {
|
||||
FloatingActionButton(onClick = onClick, contentColor = MaterialTheme.colorScheme.primary) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Download,
|
||||
imageVector = MeshtasticIcons.Download,
|
||||
contentDescription = stringResource(Res.string.map_download_region),
|
||||
modifier = Modifier.scale(1.25f),
|
||||
)
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* 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 org.meshtastic.feature.map.component
|
||||
package org.meshtastic.app.map.component
|
||||
|
||||
import android.app.DatePickerDialog
|
||||
import android.widget.DatePicker
|
||||
|
|
@ -34,9 +34,6 @@ import androidx.compose.foundation.shape.CircleShape
|
|||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.CalendarMonth
|
||||
import androidx.compose.material.icons.rounded.Lock
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.IconButton
|
||||
|
|
@ -60,16 +57,13 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.Month
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.nowInstant
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.common.util.systemTimeZone
|
||||
import org.meshtastic.core.common.util.toDate
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.cancel
|
||||
import org.meshtastic.core.resources.date
|
||||
|
|
@ -84,9 +78,13 @@ import org.meshtastic.core.resources.waypoint_edit
|
|||
import org.meshtastic.core.resources.waypoint_new
|
||||
import org.meshtastic.core.ui.component.EditTextPreference
|
||||
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
|
||||
import org.meshtastic.core.ui.icon.CalendarMonth
|
||||
import org.meshtastic.core.ui.icon.Lock
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.proto.Waypoint
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Instant
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
|
|
@ -102,7 +100,7 @@ fun EditWaypointDialog(
|
|||
val title = if (waypoint.id == 0) Res.string.waypoint_new else Res.string.waypoint_edit
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
val emoji = if ((waypointInput.icon ?: 0) == 0) 128205 else waypointInput.icon!!
|
||||
val emoji = if (waypointInput.icon == 0) 128205 else waypointInput.icon
|
||||
var showEmojiPickerView by remember { mutableStateOf(false) }
|
||||
|
||||
// Get current context for dialogs
|
||||
|
|
@ -117,11 +115,11 @@ fun EditWaypointDialog(
|
|||
|
||||
val currentInstant =
|
||||
remember(waypointInput.expire) {
|
||||
val expire = waypointInput.expire ?: 0
|
||||
val expire = waypointInput.expire
|
||||
if (expire != 0 && expire != Int.MAX_VALUE) {
|
||||
Instant.fromEpochSeconds(expire.toLong())
|
||||
kotlin.time.Instant.fromEpochSeconds(expire.toLong())
|
||||
} else {
|
||||
nowInstant + 8.hours
|
||||
kotlin.time.Clock.System.now() + 8.hours
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -129,8 +127,8 @@ fun EditWaypointDialog(
|
|||
var selectedDate by
|
||||
remember(currentInstant) {
|
||||
mutableStateOf(
|
||||
if ((waypointInput.expire ?: 0) != 0 && waypointInput.expire != Int.MAX_VALUE) {
|
||||
dateFormat.format(currentInstant.toDate())
|
||||
if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) {
|
||||
dateFormat.format(java.util.Date(currentInstant.toEpochMilliseconds()))
|
||||
} else {
|
||||
""
|
||||
},
|
||||
|
|
@ -139,8 +137,8 @@ fun EditWaypointDialog(
|
|||
var selectedTime by
|
||||
remember(currentInstant) {
|
||||
mutableStateOf(
|
||||
if ((waypointInput.expire ?: 0) != 0 && waypointInput.expire != Int.MAX_VALUE) {
|
||||
timeFormat.format(currentInstant.toDate())
|
||||
if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) {
|
||||
timeFormat.format(java.util.Date(currentInstant.toEpochMilliseconds()))
|
||||
} else {
|
||||
""
|
||||
},
|
||||
|
|
@ -164,7 +162,7 @@ fun EditWaypointDialog(
|
|||
)
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.name),
|
||||
value = waypointInput.name ?: "",
|
||||
value = waypointInput.name,
|
||||
maxSize = 29,
|
||||
enabled = true,
|
||||
isError = false,
|
||||
|
|
@ -187,7 +185,7 @@ fun EditWaypointDialog(
|
|||
)
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.description),
|
||||
value = waypointInput.description ?: "",
|
||||
value = waypointInput.description,
|
||||
maxSize = 99,
|
||||
enabled = true,
|
||||
isError = false,
|
||||
|
|
@ -200,11 +198,14 @@ fun EditWaypointDialog(
|
|||
modifier = Modifier.fillMaxWidth().size(48.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(imageVector = Icons.Rounded.Lock, contentDescription = stringResource(Res.string.locked))
|
||||
Image(
|
||||
imageVector = MeshtasticIcons.Lock,
|
||||
contentDescription = stringResource(Res.string.locked),
|
||||
)
|
||||
Text(stringResource(Res.string.locked))
|
||||
Switch(
|
||||
modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End),
|
||||
checked = (waypointInput.locked_to ?: 0) != 0,
|
||||
checked = waypointInput.locked_to != 0,
|
||||
onCheckedChange = { waypointInput = waypointInput.copy(locked_to = if (it) 1 else 0) },
|
||||
)
|
||||
}
|
||||
|
|
@ -227,7 +228,7 @@ fun EditWaypointDialog(
|
|||
waypointInput = waypointInput.copy(expire = newLdt.toInstant(tz).epochSeconds.toInt())
|
||||
},
|
||||
ldt.year,
|
||||
ldt.monthNumber - 1,
|
||||
ldt.month.ordinal,
|
||||
ldt.day,
|
||||
)
|
||||
|
||||
|
|
@ -257,13 +258,13 @@ fun EditWaypointDialog(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(
|
||||
imageVector = Icons.Rounded.CalendarMonth,
|
||||
imageVector = MeshtasticIcons.CalendarMonth,
|
||||
contentDescription = stringResource(Res.string.expires),
|
||||
)
|
||||
Text(stringResource(Res.string.expires))
|
||||
Switch(
|
||||
modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End),
|
||||
checked = waypointInput.expire != Int.MAX_VALUE && (waypointInput.expire ?: 0) != 0,
|
||||
checked = waypointInput.expire != Int.MAX_VALUE && waypointInput.expire != 0,
|
||||
onCheckedChange = { isChecked ->
|
||||
if (isChecked) {
|
||||
waypointInput = waypointInput.copy(expire = currentInstant.epochSeconds.toInt())
|
||||
|
|
@ -274,7 +275,7 @@ fun EditWaypointDialog(
|
|||
)
|
||||
}
|
||||
|
||||
if (waypointInput.expire != Int.MAX_VALUE && (waypointInput.expire ?: 0) != 0) {
|
||||
if (waypointInput.expire != Int.MAX_VALUE && waypointInput.expire != 0) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* 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
|
||||
|
|
@ -14,8 +14,7 @@
|
|||
* 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 org.meshtastic.feature.map.model
|
||||
package org.meshtastic.app.map.model
|
||||
|
||||
import org.osmdroid.tileprovider.tilesource.ITileSource
|
||||
import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* 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
|
||||
|
|
@ -14,16 +14,15 @@
|
|||
* 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 org.meshtastic.feature.map.model
|
||||
package org.meshtastic.app.map.model
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.view.MotionEvent
|
||||
import org.meshtastic.feature.map.dpToPx
|
||||
import org.meshtastic.feature.map.spToPx
|
||||
import org.meshtastic.app.map.dpToPx
|
||||
import org.meshtastic.app.map.spToPx
|
||||
import org.osmdroid.views.MapView
|
||||
import org.osmdroid.views.overlay.Marker
|
||||
import org.osmdroid.views.overlay.Polygon
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* 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 org.meshtastic.feature.map.model
|
||||
package org.meshtastic.app.map.model
|
||||
|
||||
import android.content.res.Resources
|
||||
import co.touchlab.kermit.Logger
|
||||
|
|
@ -86,22 +86,6 @@ open class NOAAWmsTileSource(
|
|||
if (time != null) this.time = time
|
||||
}
|
||||
|
||||
// fun createFrom(endpoint: WMSEndpoint, layer: WMSLayer): WMSTileSource? {
|
||||
// var srs: String? = "EPSG:900913"
|
||||
// if (layer.srs.isNotEmpty()) {
|
||||
// srs = layer.srs[0]
|
||||
// }
|
||||
// return if (layer.styles.isEmpty()) {
|
||||
// WMSTileSource(
|
||||
// layer.name, arrayOf(endpoint.baseurl), layer.name,
|
||||
// endpoint.wmsVersion, srs, null, layer.pixelSize
|
||||
// )
|
||||
// } else WMSTileSource(
|
||||
// layer.name, arrayOf(endpoint.baseurl), layer.name,
|
||||
// endpoint.wmsVersion, srs, layer.styles[0], layer.pixelSize
|
||||
// )
|
||||
// }
|
||||
|
||||
private fun tile2lon(x: Int, z: Int): Double = x / 2.0.pow(z.toDouble()) * 360.0 - 180
|
||||
|
||||
private fun tile2lat(y: Int, z: Int): Double {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* 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
|
||||
|
|
@ -14,8 +14,7 @@
|
|||
* 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 org.meshtastic.feature.map.model
|
||||
package org.meshtastic.app.map.model
|
||||
|
||||
import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase
|
||||
import org.osmdroid.tileprovider.tilesource.TileSourcePolicy
|
||||
|
|
@ -14,9 +14,9 @@
|
|||
* 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 org.meshtastic.feature.map.node
|
||||
package org.meshtastic.app.map.node
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -24,13 +24,12 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.feature.map.MapView
|
||||
import org.meshtastic.feature.map.node.NodeMapViewModel
|
||||
|
||||
@Composable
|
||||
fun NodeMapScreen(nodeMapViewModel: NodeMapViewModel, onNavigateUp: () -> Unit) {
|
||||
val node by nodeMapViewModel.node.collectAsStateWithLifecycle()
|
||||
val positions by nodeMapViewModel.positionLogs.collectAsStateWithLifecycle()
|
||||
val destNum = node?.num
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
|
|
@ -45,8 +44,11 @@ fun NodeMapScreen(nodeMapViewModel: NodeMapViewModel, onNavigateUp: () -> Unit)
|
|||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
Box(modifier = Modifier.padding(paddingValues)) {
|
||||
MapView(focusedNodeNum = destNum, nodeTracks = positions, navigateToNodeDetails = {})
|
||||
}
|
||||
NodeTrackOsmMap(
|
||||
positions = positions,
|
||||
applicationId = nodeMapViewModel.applicationId,
|
||||
mapStyleId = nodeMapViewModel.mapStyleId,
|
||||
modifier = Modifier.fillMaxSize().padding(paddingValues),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* 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 org.meshtastic.app.map.node
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.meshtastic.feature.map.node.NodeMapViewModel
|
||||
import org.meshtastic.proto.Position
|
||||
|
||||
/**
|
||||
* Flavor-unified entry point for the embeddable node-track map. Resolves [destNum] to obtain
|
||||
* [NodeMapViewModel.applicationId] and [NodeMapViewModel.mapStyleId], then delegates to the OSMDroid implementation
|
||||
* ([NodeTrackOsmMap]).
|
||||
*
|
||||
* Supports optional synchronized selection via [selectedPositionTime] and [onPositionSelected].
|
||||
*/
|
||||
@Composable
|
||||
fun NodeTrackMap(
|
||||
destNum: Int,
|
||||
positions: List<Position>,
|
||||
modifier: Modifier = Modifier,
|
||||
selectedPositionTime: Int? = null,
|
||||
onPositionSelected: ((Int) -> Unit)? = null,
|
||||
) {
|
||||
val vm = koinViewModel<NodeMapViewModel>()
|
||||
vm.setDestNum(destNum)
|
||||
NodeTrackOsmMap(
|
||||
positions = positions,
|
||||
applicationId = vm.applicationId,
|
||||
mapStyleId = vm.mapStyleId,
|
||||
modifier = modifier,
|
||||
selectedPositionTime = selectedPositionTime,
|
||||
onPositionSelected = onPositionSelected,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* 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 org.meshtastic.app.map.node
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.meshtastic.app.map.MapViewModel
|
||||
import org.meshtastic.app.map.addCopyright
|
||||
import org.meshtastic.app.map.addPolyline
|
||||
import org.meshtastic.app.map.addPositionMarkers
|
||||
import org.meshtastic.app.map.addScaleBarOverlay
|
||||
import org.meshtastic.app.map.model.CustomTileSource
|
||||
import org.meshtastic.app.map.rememberMapViewWithLifecycle
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.model.util.GeoConstants.DEG_D
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.last_heard_filter_label
|
||||
import org.meshtastic.feature.map.LastHeardFilter
|
||||
import org.meshtastic.feature.map.component.MapControlsOverlay
|
||||
import org.meshtastic.proto.Position
|
||||
import org.osmdroid.util.BoundingBox
|
||||
import org.osmdroid.util.GeoPoint
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* A focused OSMDroid map composable that renders **only** a node's position track — a dashed polyline with directional
|
||||
* markers for each historical position.
|
||||
*
|
||||
* Applies the [lastHeardTrackFilter][org.meshtastic.feature.map.BaseMapViewModel.MapFilterState.lastHeardTrackFilter]
|
||||
* from [MapViewModel] to filter positions by time, matching the behavior of the Google Maps implementation. Includes a
|
||||
* minimal [MapControlsOverlay][org.meshtastic.feature.map.component.MapControlsOverlay] with a track time filter slider
|
||||
* so users can adjust the time range directly from the map.
|
||||
*
|
||||
* Supports optional synchronized selection via [selectedPositionTime] and [onPositionSelected].
|
||||
*
|
||||
* Unlike the main [org.meshtastic.app.map.MapView], this composable does **not** include node clusters, waypoints, or
|
||||
* location tracking. It is designed to be embedded inside the position-log adaptive layout.
|
||||
*/
|
||||
@Composable
|
||||
fun NodeTrackOsmMap(
|
||||
positions: List<Position>,
|
||||
applicationId: String,
|
||||
mapStyleId: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
selectedPositionTime: Int? = null,
|
||||
onPositionSelected: ((Int) -> Unit)? = null,
|
||||
mapViewModel: MapViewModel = koinViewModel(),
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val mapFilterState by mapViewModel.mapFilterStateFlow.collectAsStateWithLifecycle()
|
||||
val lastHeardTrackFilter = mapFilterState.lastHeardTrackFilter
|
||||
|
||||
val filteredPositions =
|
||||
remember(positions, lastHeardTrackFilter) {
|
||||
positions.filter {
|
||||
lastHeardTrackFilter == LastHeardFilter.Any || it.time > nowSeconds - lastHeardTrackFilter.seconds
|
||||
}
|
||||
}
|
||||
|
||||
val geoPoints =
|
||||
remember(filteredPositions) {
|
||||
filteredPositions.map { GeoPoint((it.latitude_i ?: 0) * DEG_D, (it.longitude_i ?: 0) * DEG_D) }
|
||||
}
|
||||
val cameraView = remember(geoPoints) { BoundingBox.fromGeoPoints(geoPoints) }
|
||||
val mapView =
|
||||
rememberMapViewWithLifecycle(
|
||||
applicationId = applicationId,
|
||||
box = cameraView,
|
||||
tileSource = CustomTileSource.getTileSource(mapStyleId),
|
||||
)
|
||||
|
||||
var filterMenuExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Box(modifier = modifier) {
|
||||
AndroidView(
|
||||
modifier = Modifier.matchParentSize(),
|
||||
factory = { mapView },
|
||||
update = { map ->
|
||||
map.overlays.clear()
|
||||
map.addCopyright()
|
||||
map.addScaleBarOverlay(density)
|
||||
map.addPolyline(density, geoPoints) {}
|
||||
map.addPositionMarkers(filteredPositions) { time -> onPositionSelected?.invoke(time) }
|
||||
// Center on selected position
|
||||
if (selectedPositionTime != null) {
|
||||
val selected = filteredPositions.find { it.time == selectedPositionTime }
|
||||
if (selected != null) {
|
||||
val point = GeoPoint((selected.latitude_i ?: 0) * DEG_D, (selected.longitude_i ?: 0) * DEG_D)
|
||||
map.controller.animateTo(point)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Track filter controls overlay
|
||||
MapControlsOverlay(
|
||||
modifier = Modifier.align(Alignment.TopCenter).padding(top = 8.dp),
|
||||
onToggleFilterMenu = { filterMenuExpanded = true },
|
||||
filterDropdownContent = {
|
||||
DropdownMenu(expanded = filterMenuExpanded, onDismissRequest = { filterMenuExpanded = false }) {
|
||||
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
|
||||
val filterOptions = LastHeardFilter.entries
|
||||
val selectedIndex = filterOptions.indexOf(lastHeardTrackFilter)
|
||||
var sliderPosition by remember(selectedIndex) { mutableFloatStateOf(selectedIndex.toFloat()) }
|
||||
|
||||
Text(
|
||||
text =
|
||||
stringResource(
|
||||
Res.string.last_heard_filter_label,
|
||||
stringResource(lastHeardTrackFilter.label),
|
||||
),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
Slider(
|
||||
value = sliderPosition,
|
||||
onValueChange = { sliderPosition = it },
|
||||
onValueChangeFinished = {
|
||||
val newIndex = sliderPosition.roundToInt().coerceIn(0, filterOptions.size - 1)
|
||||
mapViewModel.setLastHeardTrackFilter(filterOptions[newIndex])
|
||||
},
|
||||
valueRange = 0f..(filterOptions.size - 1).toFloat(),
|
||||
steps = filterOptions.size - 2,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* 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 org.meshtastic.app.map.traceroute
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import org.meshtastic.core.model.TracerouteOverlay
|
||||
import org.meshtastic.proto.Position
|
||||
|
||||
/**
|
||||
* Flavor-unified entry point for the embeddable traceroute map. Delegates to the OSMDroid implementation
|
||||
* ([TracerouteOsmMap]).
|
||||
*/
|
||||
@Composable
|
||||
fun TracerouteMap(
|
||||
tracerouteOverlay: TracerouteOverlay?,
|
||||
tracerouteNodePositions: Map<Int, Position>,
|
||||
onMappableCountChanged: (shown: Int, total: Int) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TracerouteOsmMap(
|
||||
tracerouteOverlay = tracerouteOverlay,
|
||||
tracerouteNodePositions = tracerouteNodePositions,
|
||||
onMappableCountChanged = onMappableCountChanged,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
@file:Suppress("MagicNumber")
|
||||
|
||||
package org.meshtastic.app.map.traceroute
|
||||
|
||||
import android.graphics.Paint
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.meshtastic.app.R
|
||||
import org.meshtastic.app.map.MapViewModel
|
||||
import org.meshtastic.app.map.addCopyright
|
||||
import org.meshtastic.app.map.addScaleBarOverlay
|
||||
import org.meshtastic.app.map.model.CustomTileSource
|
||||
import org.meshtastic.app.map.model.MarkerWithLabel
|
||||
import org.meshtastic.app.map.rememberMapViewWithLifecycle
|
||||
import org.meshtastic.app.map.zoomIn
|
||||
import org.meshtastic.core.model.TracerouteOverlay
|
||||
import org.meshtastic.core.model.util.GeoConstants.EARTH_RADIUS_METERS
|
||||
import org.meshtastic.core.ui.theme.TracerouteColors
|
||||
import org.meshtastic.core.ui.util.formatAgo
|
||||
import org.meshtastic.feature.map.tracerouteNodeSelection
|
||||
import org.meshtastic.proto.Position
|
||||
import org.osmdroid.util.BoundingBox
|
||||
import org.osmdroid.util.GeoPoint
|
||||
import org.osmdroid.views.overlay.Marker
|
||||
import org.osmdroid.views.overlay.Polyline
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.asin
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
private const val TRACEROUTE_OFFSET_METERS = 100.0
|
||||
private const val TRACEROUTE_SINGLE_POINT_ZOOM = 12.0
|
||||
private const val TRACEROUTE_ZOOM_OUT_LEVELS = 0.5
|
||||
|
||||
/**
|
||||
* A focused OSMDroid map composable that renders **only** traceroute visualization — node markers for each hop and
|
||||
* forward/return offset polylines with auto-centering camera.
|
||||
*
|
||||
* Unlike the main `MapView`, this composable does **not** include node clusters, waypoints, location tracking, or any
|
||||
* map controls. It is designed to be embedded inside `TracerouteMapScreen`'s scaffold.
|
||||
*/
|
||||
@Composable
|
||||
fun TracerouteOsmMap(
|
||||
tracerouteOverlay: TracerouteOverlay?,
|
||||
tracerouteNodePositions: Map<Int, Position>,
|
||||
onMappableCountChanged: (shown: Int, total: Int) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
mapViewModel: MapViewModel = koinViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val density = LocalDensity.current
|
||||
val nodes by mapViewModel.nodes.collectAsStateWithLifecycle()
|
||||
val markerIcon = remember { AppCompatResources.getDrawable(context, R.drawable.ic_location_on) }
|
||||
|
||||
// Resolve which nodes to display for the traceroute
|
||||
val tracerouteSelection =
|
||||
remember(tracerouteOverlay, tracerouteNodePositions, nodes) {
|
||||
mapViewModel.tracerouteNodeSelection(
|
||||
tracerouteOverlay = tracerouteOverlay,
|
||||
tracerouteNodePositions = tracerouteNodePositions,
|
||||
nodes = nodes,
|
||||
)
|
||||
}
|
||||
val displayNodes = tracerouteSelection.nodesForMarkers
|
||||
val nodeLookup = tracerouteSelection.nodeLookup
|
||||
|
||||
// Report mappable count
|
||||
LaunchedEffect(tracerouteOverlay, displayNodes) {
|
||||
if (tracerouteOverlay != null) {
|
||||
onMappableCountChanged(displayNodes.size, tracerouteOverlay.relatedNodeNums.size)
|
||||
}
|
||||
}
|
||||
|
||||
// Compute polyline GeoPoints from node positions
|
||||
val forwardPoints =
|
||||
remember(tracerouteOverlay, nodeLookup) {
|
||||
tracerouteOverlay?.forwardRoute?.mapNotNull {
|
||||
nodeLookup[it]?.let { node -> GeoPoint(node.latitude, node.longitude) }
|
||||
} ?: emptyList()
|
||||
}
|
||||
val returnPoints =
|
||||
remember(tracerouteOverlay, nodeLookup) {
|
||||
tracerouteOverlay?.returnRoute?.mapNotNull {
|
||||
nodeLookup[it]?.let { node -> GeoPoint(node.latitude, node.longitude) }
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
// Compute offset polylines for visual separation
|
||||
val headingReferencePoints =
|
||||
remember(forwardPoints, returnPoints) {
|
||||
when {
|
||||
forwardPoints.size >= 2 -> forwardPoints
|
||||
returnPoints.size >= 2 -> returnPoints
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
val forwardOffsetPoints =
|
||||
remember(forwardPoints, headingReferencePoints) {
|
||||
offsetPolyline(
|
||||
points = forwardPoints,
|
||||
offsetMeters = TRACEROUTE_OFFSET_METERS,
|
||||
headingReferencePoints = headingReferencePoints,
|
||||
sideMultiplier = 1.0,
|
||||
)
|
||||
}
|
||||
val returnOffsetPoints =
|
||||
remember(returnPoints, headingReferencePoints) {
|
||||
offsetPolyline(
|
||||
points = returnPoints,
|
||||
offsetMeters = TRACEROUTE_OFFSET_METERS,
|
||||
headingReferencePoints = headingReferencePoints,
|
||||
sideMultiplier = -1.0,
|
||||
)
|
||||
}
|
||||
|
||||
// Camera auto-center
|
||||
var hasCentered by remember(tracerouteOverlay) { mutableStateOf(false) }
|
||||
|
||||
// Build initial camera from all traceroute points
|
||||
val allPoints = remember(forwardPoints, returnPoints) { (forwardPoints + returnPoints).distinct() }
|
||||
val initialCameraView =
|
||||
remember(allPoints) { if (allPoints.isEmpty()) null else BoundingBox.fromGeoPoints(allPoints) }
|
||||
|
||||
val mapView =
|
||||
rememberMapViewWithLifecycle(
|
||||
applicationId = mapViewModel.applicationId,
|
||||
box = initialCameraView ?: BoundingBox(),
|
||||
tileSource = CustomTileSource.getTileSource(mapViewModel.mapStyleId),
|
||||
)
|
||||
|
||||
// Center camera on traceroute bounds
|
||||
LaunchedEffect(tracerouteOverlay, forwardPoints, returnPoints) {
|
||||
if (tracerouteOverlay == null || hasCentered) return@LaunchedEffect
|
||||
if (allPoints.isNotEmpty()) {
|
||||
if (allPoints.size == 1) {
|
||||
mapView.controller.setCenter(allPoints.first())
|
||||
mapView.controller.setZoom(TRACEROUTE_SINGLE_POINT_ZOOM)
|
||||
} else {
|
||||
mapView.zoomToBoundingBox(
|
||||
BoundingBox.fromGeoPoints(allPoints).zoomIn(-TRACEROUTE_ZOOM_OUT_LEVELS),
|
||||
true,
|
||||
)
|
||||
}
|
||||
hasCentered = true
|
||||
}
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
modifier = modifier,
|
||||
factory = { mapView.apply { setDestroyMode(false) } },
|
||||
update = { map ->
|
||||
map.overlays.clear()
|
||||
map.addCopyright()
|
||||
map.addScaleBarOverlay(density)
|
||||
|
||||
// Render traceroute polylines
|
||||
buildTraceroutePolylines(forwardOffsetPoints, returnOffsetPoints, density).forEach { map.overlays.add(it) }
|
||||
|
||||
// Render simple node markers
|
||||
displayNodes.forEach { node ->
|
||||
val position = GeoPoint(node.latitude, node.longitude)
|
||||
val marker =
|
||||
MarkerWithLabel(mapView = map, label = "${node.user.short_name} ${formatAgo(node.position.time)}")
|
||||
.apply {
|
||||
id = node.user.id
|
||||
title = node.user.long_name
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||
this.position = position
|
||||
icon = markerIcon
|
||||
setNodeColors(node.colors)
|
||||
}
|
||||
map.overlays.add(marker)
|
||||
}
|
||||
|
||||
map.invalidate()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildTraceroutePolylines(
|
||||
forwardPoints: List<GeoPoint>,
|
||||
returnPoints: List<GeoPoint>,
|
||||
density: androidx.compose.ui.unit.Density,
|
||||
): List<Polyline> {
|
||||
val polylines = mutableListOf<Polyline>()
|
||||
|
||||
fun buildPolyline(points: List<GeoPoint>, color: Int, strokeWidth: Float): Polyline = Polyline().apply {
|
||||
setPoints(points)
|
||||
outlinePaint.apply {
|
||||
this.color = color
|
||||
this.strokeWidth = strokeWidth
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
style = Paint.Style.STROKE
|
||||
}
|
||||
}
|
||||
|
||||
forwardPoints
|
||||
.takeIf { it.size >= 2 }
|
||||
?.let { points ->
|
||||
polylines.add(buildPolyline(points, TracerouteColors.OutgoingRoute.toArgb(), with(density) { 6.dp.toPx() }))
|
||||
}
|
||||
returnPoints
|
||||
.takeIf { it.size >= 2 }
|
||||
?.let { points ->
|
||||
polylines.add(buildPolyline(points, TracerouteColors.ReturnRoute.toArgb(), with(density) { 5.dp.toPx() }))
|
||||
}
|
||||
return polylines
|
||||
}
|
||||
|
||||
// --- Haversine offset math for OSMDroid (no SphericalUtil available) ---
|
||||
|
||||
private fun Double.toRad(): Double = this * PI / 180.0
|
||||
|
||||
private fun bearingRad(from: GeoPoint, to: GeoPoint): Double {
|
||||
val lat1 = from.latitude.toRad()
|
||||
val lat2 = to.latitude.toRad()
|
||||
val dLon = (to.longitude - from.longitude).toRad()
|
||||
return atan2(sin(dLon) * cos(lat2), cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon))
|
||||
}
|
||||
|
||||
private fun GeoPoint.offsetPoint(headingRad: Double, offsetMeters: Double): GeoPoint {
|
||||
val distanceByRadius = offsetMeters / EARTH_RADIUS_METERS
|
||||
val lat1 = latitude.toRad()
|
||||
val lon1 = longitude.toRad()
|
||||
val lat2 = asin(sin(lat1) * cos(distanceByRadius) + cos(lat1) * sin(distanceByRadius) * cos(headingRad))
|
||||
val lon2 =
|
||||
lon1 + atan2(sin(headingRad) * sin(distanceByRadius) * cos(lat1), cos(distanceByRadius) - sin(lat1) * sin(lat2))
|
||||
return GeoPoint(lat2 * 180.0 / PI, lon2 * 180.0 / PI)
|
||||
}
|
||||
|
||||
private fun offsetPolyline(
|
||||
points: List<GeoPoint>,
|
||||
offsetMeters: Double,
|
||||
headingReferencePoints: List<GeoPoint> = points,
|
||||
sideMultiplier: Double = 1.0,
|
||||
): List<GeoPoint> {
|
||||
val headingPoints = headingReferencePoints.takeIf { it.size >= 2 } ?: points
|
||||
if (points.size < 2 || headingPoints.size < 2 || offsetMeters == 0.0) return points
|
||||
|
||||
val headings =
|
||||
headingPoints.mapIndexed { index, _ ->
|
||||
when (index) {
|
||||
0 -> bearingRad(headingPoints[0], headingPoints[1])
|
||||
headingPoints.lastIndex ->
|
||||
bearingRad(headingPoints[headingPoints.lastIndex - 1], headingPoints[headingPoints.lastIndex])
|
||||
|
||||
else -> bearingRad(headingPoints[index - 1], headingPoints[index + 1])
|
||||
}
|
||||
}
|
||||
|
||||
return points.mapIndexed { index, point ->
|
||||
val heading = headings[index.coerceIn(0, headings.lastIndex)]
|
||||
val perpendicularHeading = heading + (PI / 2 * sideMultiplier)
|
||||
point.offsetPoint(perpendicularHeading, abs(offsetMeters))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* 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 org.meshtastic.app.node.component
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
|
||||
import org.osmdroid.util.GeoPoint
|
||||
import org.osmdroid.views.MapView
|
||||
import org.osmdroid.views.overlay.Marker
|
||||
|
||||
@Composable
|
||||
fun InlineMap(node: Node, modifier: Modifier = Modifier) {
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
|
||||
val map = remember {
|
||||
MapView(context).apply {
|
||||
layoutParams =
|
||||
ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||
|
||||
// Default osmdroid tile source.
|
||||
setTileSource(TileSourceFactory.MAPNIK)
|
||||
setMultiTouchControls(false)
|
||||
|
||||
controller.setZoom(15.0)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(node.num) {
|
||||
val point = GeoPoint(node.latitude, node.longitude)
|
||||
|
||||
map.overlays.clear()
|
||||
|
||||
val marker =
|
||||
Marker(map).apply {
|
||||
position = point
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||
}
|
||||
map.overlays.add(marker)
|
||||
|
||||
map.controller.animateTo(point)
|
||||
}
|
||||
|
||||
AndroidView(factory = { map }, modifier = modifier)
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* 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
|
||||
|
|
@ -14,15 +14,15 @@
|
|||
* 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 org.meshtastic.feature.node.metrics
|
||||
package org.meshtastic.app.node.metrics
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.meshtastic.core.ui.util.TracerouteMapOverlayInsets
|
||||
|
||||
internal object TracerouteMapOverlayInsets {
|
||||
val overlayAlignment: Alignment = Alignment.BottomEnd
|
||||
val overlayPadding: PaddingValues = PaddingValues(end = 16.dp, bottom = 16.dp)
|
||||
val contentHorizontalAlignment: Alignment.Horizontal = Alignment.End
|
||||
}
|
||||
fun getTracerouteMapOverlayInsets(): TracerouteMapOverlayInsets = TracerouteMapOverlayInsets(
|
||||
overlayAlignment = Alignment.BottomEnd,
|
||||
overlayPadding = PaddingValues(end = 16.dp, bottom = 16.dp),
|
||||
contentHorizontalAlignment = Alignment.End,
|
||||
)
|
||||
|
|
@ -14,22 +14,18 @@
|
|||
* 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 org.meshtastic.core.analytics.platform
|
||||
package org.meshtastic.app.analytics
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavHostController
|
||||
import co.touchlab.kermit.LogWriter
|
||||
import co.touchlab.kermit.Severity
|
||||
import com.datadog.android.Datadog
|
||||
import com.datadog.android.DatadogSite
|
||||
import com.datadog.android.compose.ExperimentalTrackingApi
|
||||
import com.datadog.android.compose.NavigationViewTrackingEffect
|
||||
import com.datadog.android.core.configuration.Configuration
|
||||
import com.datadog.android.log.Logger
|
||||
import com.datadog.android.log.Logs
|
||||
|
|
@ -37,8 +33,11 @@ import com.datadog.android.log.LogsConfiguration
|
|||
import com.datadog.android.privacy.TrackingConsent
|
||||
import com.datadog.android.rum.GlobalRumMonitor
|
||||
import com.datadog.android.rum.Rum
|
||||
import com.datadog.android.rum.RumActionType
|
||||
import com.datadog.android.rum.RumConfiguration
|
||||
import com.datadog.android.rum.tracking.AcceptAllNavDestinations
|
||||
import com.datadog.android.sessionreplay.SessionReplay
|
||||
import com.datadog.android.sessionreplay.SessionReplayConfiguration
|
||||
import com.datadog.android.sessionreplay.TextAndInputPrivacy
|
||||
import com.datadog.android.trace.Trace
|
||||
import com.datadog.android.trace.TraceConfiguration
|
||||
import com.datadog.android.trace.opentelemetry.DatadogOpenTelemetry
|
||||
|
|
@ -51,15 +50,15 @@ import com.google.firebase.analytics.analytics
|
|||
import com.google.firebase.crashlytics.crashlytics
|
||||
import com.google.firebase.crashlytics.setCustomKeys
|
||||
import com.google.firebase.initialize
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.opentelemetry.api.GlobalOpenTelemetry
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.meshtastic.core.analytics.BuildConfig
|
||||
import org.meshtastic.core.analytics.DataPair
|
||||
import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
|
||||
import javax.inject.Inject
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.app.BuildConfig
|
||||
import org.meshtastic.core.repository.AnalyticsPrefs
|
||||
import org.meshtastic.core.repository.DataPair
|
||||
import org.meshtastic.core.repository.PlatformAnalytics
|
||||
import co.touchlab.kermit.Logger as KermitLogger
|
||||
|
||||
/**
|
||||
|
|
@ -69,14 +68,11 @@ import co.touchlab.kermit.Logger as KermitLogger
|
|||
* This implementation delays initialization of SDKs until user consent is granted to reduce tracking "noise" and
|
||||
* respect privacy-focused environments.
|
||||
*/
|
||||
class GooglePlatformAnalytics
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val analyticsPrefs: AnalyticsPrefs,
|
||||
) : PlatformAnalytics {
|
||||
@Single
|
||||
class GooglePlatformAnalytics(private val context: Context, private val analyticsPrefs: AnalyticsPrefs) :
|
||||
PlatformAnalytics {
|
||||
|
||||
private val sampleRate = 100f.takeIf { BuildConfig.DEBUG } ?: 10f // For Datadog remote sample rate
|
||||
private val sampleRate = 100f // Match Apple: 100% sampling for cross-platform DataDog comparison
|
||||
|
||||
private var datadogLogger: Logger? = null
|
||||
private var isFirebaseInitialized = false
|
||||
|
|
@ -109,11 +105,10 @@ constructor(
|
|||
KermitLogger.setMinSeverity(if (BuildConfig.DEBUG) Severity.Debug else Severity.Info)
|
||||
|
||||
// Initial consent state
|
||||
updateAnalyticsConsent(analyticsPrefs.analyticsAllowed)
|
||||
updateAnalyticsConsent(analyticsPrefs.analyticsAllowed.value)
|
||||
|
||||
// Subscribe to analytics preference changes
|
||||
analyticsPrefs
|
||||
.getAnalyticsAllowedChangesFlow()
|
||||
analyticsPrefs.analyticsAllowed
|
||||
.onEach { allowed -> updateAnalyticsConsent(allowed) }
|
||||
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||
}
|
||||
|
|
@ -122,7 +117,7 @@ constructor(
|
|||
* Ensures that Datadog and Firebase SDKs are initialized if allowed. This is called lazily when consent is granted.
|
||||
*/
|
||||
private fun ensureInitialized() {
|
||||
if (!analyticsPrefs.analyticsAllowed || isInTestLab) return
|
||||
if (!analyticsPrefs.analyticsAllowed.value || isInTestLab) return
|
||||
|
||||
if (!Datadog.isInitialized()) {
|
||||
initDatadog(context as Application)
|
||||
|
|
@ -146,7 +141,7 @@ constructor(
|
|||
val configuration =
|
||||
Configuration.Builder(
|
||||
clientToken = BuildConfig.datadogClientToken,
|
||||
env = if (BuildConfig.DEBUG) "debug" else "release",
|
||||
env = if (BuildConfig.DEBUG) "Local" else "Production",
|
||||
variant = BuildConfig.FLAVOR,
|
||||
)
|
||||
.useSite(DatadogSite.US5)
|
||||
|
|
@ -160,7 +155,7 @@ constructor(
|
|||
val rumConfiguration =
|
||||
RumConfiguration.Builder(BuildConfig.datadogApplicationId)
|
||||
.trackAnonymousUser(true)
|
||||
.trackBackgroundEvents(false) // Disable background noise
|
||||
.trackBackgroundEvents(true) // Match Apple: track background events for cross-platform parity
|
||||
.trackFrustrations(false) // Disable click-tracking based frustration detection
|
||||
.trackLongTasks()
|
||||
.trackNonFatalAnrs(true)
|
||||
|
|
@ -171,12 +166,20 @@ constructor(
|
|||
val logsConfig = LogsConfiguration.Builder().build()
|
||||
Logs.enable(logsConfig)
|
||||
|
||||
val traceConfig = TraceConfiguration.Builder().setNetworkInfoEnabled(false).build()
|
||||
val traceConfig = TraceConfiguration.Builder().setNetworkInfoEnabled(true).build()
|
||||
Trace.enable(traceConfig)
|
||||
|
||||
GlobalOpenTelemetry.set(DatadogOpenTelemetry(serviceName = SERVICE_NAME))
|
||||
// Session Replay for debug builds only, matching Apple's TestFlight-only gating.
|
||||
// Masks all text inputs to protect message content.
|
||||
if (BuildConfig.DEBUG) {
|
||||
val sessionReplayConfig =
|
||||
SessionReplayConfiguration.Builder(sampleRate)
|
||||
.setTextAndInputPrivacy(TextAndInputPrivacy.MASK_ALL_INPUTS)
|
||||
.build()
|
||||
SessionReplay.enable(sessionReplayConfig)
|
||||
}
|
||||
|
||||
// Session Replay disabled to reduce PII collection
|
||||
GlobalOpenTelemetry.set(DatadogOpenTelemetry(serviceName = SERVICE_NAME))
|
||||
}
|
||||
|
||||
private fun initCrashlytics(application: Application) {
|
||||
|
|
@ -244,16 +247,22 @@ constructor(
|
|||
GlobalRumMonitor.get().addAttribute("device_hardware", model)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTrackingApi::class)
|
||||
@Composable
|
||||
override fun AddNavigationTrackingEffect(navController: NavHostController) {
|
||||
if (Datadog.isInitialized()) {
|
||||
NavigationViewTrackingEffect(
|
||||
navController = navController,
|
||||
trackArguments = true,
|
||||
destinationPredicate = AcceptAllNavDestinations(),
|
||||
)
|
||||
override fun trackConnect(
|
||||
firmwareVersion: String?,
|
||||
transportType: String?,
|
||||
hardwareModel: String?,
|
||||
nodes: Int,
|
||||
connectionRestored: Boolean,
|
||||
) {
|
||||
if (!Datadog.isInitialized() || !GlobalRumMonitor.isRegistered()) return
|
||||
val attributes = buildMap {
|
||||
firmwareVersion?.let { put("firmwareVersion", it) }
|
||||
transportType?.let { put("transportType", it) }
|
||||
hardwareModel?.let { put("hardwareModel", it) }
|
||||
put("nodes", nodes)
|
||||
if (connectionRestored) put("connectionRestored", true)
|
||||
}
|
||||
GlobalRumMonitor.get().addAction(RumActionType.CUSTOM, "connect", attributes)
|
||||
}
|
||||
|
||||
private val isGooglePlayAvailable: Boolean
|
||||
|
|
@ -309,7 +318,7 @@ constructor(
|
|||
}
|
||||
|
||||
private fun String.extractSemanticVersion(): String {
|
||||
val regex = "^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?".toRegex()
|
||||
val regex = "^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?$".toRegex()
|
||||
val matchResult = regex.find(this)
|
||||
return matchResult?.groupValues?.drop(1)?.filter { it.isNotEmpty() }?.joinToString(".") ?: this
|
||||
}
|
||||
|
|
@ -318,16 +327,16 @@ constructor(
|
|||
if (!isFirebaseInitialized) return
|
||||
val bundle = Bundle()
|
||||
properties.forEach {
|
||||
when (it.value) {
|
||||
is Double -> bundle.putDouble(it.name, it.value)
|
||||
is Int ->
|
||||
bundle.putLong(it.name, it.value.toLong()) // Firebase expects Long for integer values in bundles
|
||||
is Long -> bundle.putLong(it.name, it.value)
|
||||
is Float -> bundle.putDouble(it.name, it.value.toDouble())
|
||||
is String -> bundle.putString(it.name, it.value as String?) // Explicitly handle String
|
||||
else -> bundle.putString(it.name, it.value.toString()) // Fallback for other types
|
||||
val value = it.value
|
||||
when (value) {
|
||||
is Double -> bundle.putDouble(it.name, value)
|
||||
is Int -> bundle.putLong(it.name, value.toLong()) // Firebase expects Long for integer values in bundles
|
||||
is Long -> bundle.putLong(it.name, value)
|
||||
is Float -> bundle.putDouble(it.name, value.toDouble())
|
||||
is String -> bundle.putString(it.name, value) // Explicitly handle String
|
||||
else -> bundle.putString(it.name, value.toString()) // Fallback for other types
|
||||
}
|
||||
KermitLogger.withTag(TAG).d { "Analytics: track $event (${it.name} : ${it.value})" }
|
||||
KermitLogger.withTag(TAG).d { "Analytics: track $event (${it.name} : $value)" }
|
||||
}
|
||||
Firebase.analytics.logEvent(event, bundle)
|
||||
}
|
||||
23
app/src/google/kotlin/org/meshtastic/app/di/FlavorModule.kt
Normal file
23
app/src/google/kotlin/org/meshtastic/app/di/FlavorModule.kt
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* 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 org.meshtastic.app.di
|
||||
|
||||
import org.koin.core.annotation.Module
|
||||
import org.meshtastic.app.map.prefs.di.GoogleMapsKoinModule
|
||||
|
||||
@Module(includes = [GoogleNetworkModule::class, GoogleMapsKoinModule::class])
|
||||
class FlavorModule
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* 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
|
||||
|
|
@ -14,18 +14,15 @@
|
|||
* 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 org.meshtastic.app.di
|
||||
|
||||
package com.geeksville.mesh.repository.radio
|
||||
import org.koin.core.annotation.Module
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.network.service.ApiService
|
||||
import org.meshtastic.core.network.service.ApiServiceImpl
|
||||
|
||||
import javax.inject.Inject
|
||||
@Module
|
||||
class GoogleNetworkModule {
|
||||
|
||||
/**
|
||||
* No-op interface backend implementation.
|
||||
*/
|
||||
class NopInterfaceSpec @Inject constructor(
|
||||
private val factory: NopInterfaceFactory
|
||||
) : InterfaceSpec<NopInterface> {
|
||||
override fun createInterface(rest: String): NopInterface {
|
||||
return factory.create(rest)
|
||||
}
|
||||
@Single fun bindApiService(apiServiceImpl: ApiServiceImpl): ApiService = apiServiceImpl
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* 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 org.meshtastic.feature.intro
|
||||
package org.meshtastic.app.intro
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* 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 org.meshtastic.app.map
|
||||
|
||||
import org.meshtastic.core.ui.util.MapViewProvider
|
||||
|
||||
fun getMapViewProvider(): MapViewProvider = GoogleMapViewProvider()
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* 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 org.meshtastic.app.map
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.ui.util.MapViewProvider
|
||||
|
||||
/** Google Maps implementation of [MapViewProvider]. */
|
||||
@Single
|
||||
class GoogleMapViewProvider : MapViewProvider {
|
||||
@Composable
|
||||
override fun MapView(modifier: Modifier, navigateToNodeDetails: (Int) -> Unit, waypointId: Int?) {
|
||||
val mapViewModel: MapViewModel = koinViewModel()
|
||||
LaunchedEffect(waypointId) { mapViewModel.setWaypointId(waypointId) }
|
||||
org.meshtastic.app.map.MapView(
|
||||
modifier = modifier,
|
||||
mapViewModel = mapViewModel,
|
||||
navigateToNodeDetails = navigateToNodeDetails,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* 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
|
||||
|
|
@ -14,8 +14,7 @@
|
|||
* 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 org.meshtastic.feature.map
|
||||
package org.meshtastic.app.map
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue