mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Compare commits
No commits in common. "main" and "0.1.3" have entirely different histories.
1996 changed files with 5640 additions and 211082 deletions
|
|
@ -1,27 +0,0 @@
|
|||
# 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/
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"context": {
|
||||
"fileName": ["AGENTS.md", "GEMINI.md"]
|
||||
}
|
||||
}
|
||||
13
.github/FUNDING.yml
vendored
13
.github/FUNDING.yml
vendored
|
|
@ -1,13 +0,0 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: meshtastic
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: meshtastic
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
182
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
182
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -1,182 +0,0 @@
|
|||
name: Bug Report
|
||||
description: File a bug report.
|
||||
title: "[Bug]: "
|
||||
labels: [bug]
|
||||
projects: [meshtastic/Meshtastic-Android]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for helping to make Meshtastic-Android better by reporting a bug. :hugs:
|
||||
|
||||
Please provide as much detail as possible so we can efficiently address your issue and avoid unnecessary back-and-forth.
|
||||
|
||||
- type: input
|
||||
id: contact
|
||||
attributes:
|
||||
label: Contact Details
|
||||
description: How can we get in touch with you if we need more info?
|
||||
placeholder: ex. email@example.com, discord username
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: "Checklist"
|
||||
description: |
|
||||
Please make sure you have done the following before submitting your bug report.
|
||||
Bug reports that do not meet these criteria will be closed.
|
||||
Requests that do not meet these criteria will be closed.
|
||||
Links:
|
||||
[**OPEN ISSUES**](https://github.com/meshtastic/Meshtastic-Android/issues)
|
||||
[**CLOSED ISSUES**](https://github.com/meshtastic/Meshtastic-Android/issues?q=is%3Aissue+is%3Aclosed)
|
||||
[Contribution Guidelines](https://github.com/meshtastic/Meshtastic-Android/blob/main/README.md#contributing)
|
||||
options:
|
||||
- label: |
|
||||
I am able to reproduce the bug with the latest version.
|
||||
required: true
|
||||
- label: |
|
||||
I have updated to the latest *Alpha* firmware, and am able to reproduce the bug. Many issues are fixed quickly in alpha before the general beta release.
|
||||
required: true
|
||||
- label: |
|
||||
I made sure that there are no existing **OPEN or CLOSED issues** which I could contribute my information to.
|
||||
required: true
|
||||
- label: |
|
||||
I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise.
|
||||
required: true
|
||||
- label: |
|
||||
This issue contains only one bug.
|
||||
required: true
|
||||
- label: |
|
||||
I have read and understood the **Contribution Guidelines**.
|
||||
required: true
|
||||
- label: |
|
||||
I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
- label: |
|
||||
I actually read this list, and should be taken seriously.
|
||||
required: false
|
||||
- type: input
|
||||
id: app-version
|
||||
attributes:
|
||||
label: Affected app version
|
||||
description: |
|
||||
In which Meshtastic-Android app version did you encounter the bug?
|
||||
Can be seen on the bottom of the `Settings` screen in the app.
|
||||
placeholder: "x.y.z-channel.x (build) flavor"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: phone-os
|
||||
attributes:
|
||||
label: Affected Android version
|
||||
description: |
|
||||
With what operating system (+ version) did you encounter the bug?
|
||||
placeholder: "Example: Android 14"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: phone-model
|
||||
attributes:
|
||||
label: Affected phone model
|
||||
description: |
|
||||
On what phone did you encounter the bug?
|
||||
placeholder: "Example: Samsung Galaxy S20 / Google Pixel 8"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: hardware-model
|
||||
attributes:
|
||||
label: Affected node model
|
||||
placeholder: "Example: Seeed T1000-E, Heltec v3, etc."
|
||||
description: |
|
||||
On which hardware device (Node) did you encounter the bug?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: firmware-version
|
||||
attributes:
|
||||
label: Affected node firmware version
|
||||
placeholder: "x.x.x"
|
||||
description: "Which Meshtastic firmware version did you encounter the bug?"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce the bug
|
||||
description: |
|
||||
What did you do for the bug to show up?
|
||||
|
||||
If you can't cause the bug to show up again reliably (and hence don't have a proper set of steps to give us), please still try to give as many details as possible on how you think you encountered the bug.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Press on '....'
|
||||
3. Swipe down to '....'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual-behavior
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
description: |
|
||||
Tell us what happens with the steps given above.
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: |
|
||||
Tell us what you expect to happen.
|
||||
|
||||
- type: textarea
|
||||
id: screen-media
|
||||
attributes:
|
||||
label: Screenshots/Screen recordings
|
||||
description: |
|
||||
A picture or video is worth a thousand words.
|
||||
Provide as much context as possible so we know what we are looking at.
|
||||
|
||||
Add screenshots or a screen recording to help explain your problem, provide detailed context to help us know what to look for.
|
||||
GitHub supports uploads of images and (small) videos directly in the text box.
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: |
|
||||
Logs help us to diagnose and reproduce issues, particularly when they are unique to your setup.
|
||||
Depending on the issue, the following logs may be useful:
|
||||
- App logs: This will help with most issues. If possible, provide the relevant output of:
|
||||
`adb logcat -d | grep com.geeksville.mesh`
|
||||
- Mesh logs: UI issues, communication issues etc.
|
||||
- ` App > Settings > Advanced > Debug Panel > Export specific / export all `
|
||||
- Broader Android logs: Potentially useful if the issue goes beyond the app (connections, network etc.)
|
||||
`adb logcat -d`
|
||||
- Firmware logs: Useful for all connection issues with nodes
|
||||
- These are piped to the USB serial port on the node, the most foolproof is to use the 'open serial' button on the web-flasher interface, and then save the output.
|
||||
- The app needs to be connecting to the node via Bluetooth or Network for this to work.
|
||||
render: shell
|
||||
|
||||
- type: textarea
|
||||
id: additional-information
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: |
|
||||
Any other information you'd like to include, for instance that
|
||||
* the affected device is foldable or a TV
|
||||
* you have disabled all animations on your device or otherwise changed system settings
|
||||
* you are using battery optimization or power saving mode
|
||||
* you are using a custom Android ROM or launcher
|
||||
* your ferret chewed your antennas
|
||||
* you are using a VPN
|
||||
* you live in a faraday cage
|
||||
* you dismissed all popups telling you not to do things you shouldn't do without reading them
|
||||
* ...
|
||||
10
.github/ISSUE_TEMPLATE/config.yml
vendored
10
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1,10 +0,0 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Meshtastic Android Discussions
|
||||
url: https://github.com/orgs/meshtastic/discussions/categories/android
|
||||
about: Please ask and answer questions here.
|
||||
- name: Meshtastic Website
|
||||
url: https://meshtastic.org/
|
||||
about: Docs and other ways to contact us here.
|
||||
|
||||
|
||||
61
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
61
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
|
@ -1,61 +0,0 @@
|
|||
name: Feature Request
|
||||
description: File a request for new feature or functionality.
|
||||
title: "[Feature Request]: "
|
||||
labels: [enhancement]
|
||||
projects: [meshtastic/Meshtastic-Android]
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: |
|
||||
Please make sure you have done the following before submitting your feature request.
|
||||
Requests that do not meet these criteria will be closed.
|
||||
Links:
|
||||
[**OPEN ISSUES**](https://github.com/meshtastic/Meshtastic-Android/issues)
|
||||
[**CLOSED ISSUES**](https://github.com/meshtastic/Meshtastic-Android/issues?q=is%3Aissue+is%3Aclosed)
|
||||
[Contribution Guidelines](https://github.com/meshtastic/Meshtastic-Android/blob/main/README.md#contributing)
|
||||
options:
|
||||
- label:
|
||||
I have used the search function for **OPEN ISSUES** to see if someone else has already submitted the same feature request.
|
||||
required: true
|
||||
- label: |
|
||||
I have **also** used the search function for **CLOSED ISSUES** to see if the feature was already implemented and is just waiting to be released, or if the feature was rejected.
|
||||
required: true
|
||||
- label: |
|
||||
I will describe the request with as much detail as possible.
|
||||
required: true
|
||||
- label: |
|
||||
This request contains only one single feature, **not** a list of multiple (related) features.
|
||||
required: true
|
||||
- label: |
|
||||
I have read and understood the **Contribution Guidelines**.
|
||||
required: true
|
||||
- label: |
|
||||
I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
- type: input
|
||||
id: contact
|
||||
attributes:
|
||||
label: Contact Details
|
||||
description: How can we get in touch with you if we need more info?
|
||||
placeholder: ex. email@example.com, discord username
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: feature
|
||||
attributes:
|
||||
label: Feature or improvement you want
|
||||
description: Try to be as specific as possible. Please not only explain what the feature does, but also how.
|
||||
- type: textarea
|
||||
id: reason
|
||||
attributes:
|
||||
label: Why should this be added?
|
||||
description: |
|
||||
What problem does the feature solve? In what use-cases is the feature needed?
|
||||
Is this supported by the firmware? Please provide links to relevant firmware issues or PRs if applicable.
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots / Drawings / Technical details
|
||||
description: If your request is about (or includes) changing or extending the UI, describe what the UI would look like and how the user would interact with it.
|
||||
180
.github/ISSUE_TEMPLATE/zbug_report_internal.yml
vendored
180
.github/ISSUE_TEMPLATE/zbug_report_internal.yml
vendored
|
|
@ -1,180 +0,0 @@
|
|||
name: Internal testing - Bug Report
|
||||
description: File a bug report.
|
||||
title: "[Bug]: "
|
||||
labels: [bug, ch_testing]
|
||||
projects: [meshtastic/Meshtastic-Android]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
This Bug report is only for the internal testing builds. If you are not on the list, or didn't seek one out deliberately, you should use the generic Bug Report. :hugs:
|
||||
|
||||
Please provide as much detail as possible so we can efficiently reproduce the issue.
|
||||
|
||||
- type: input
|
||||
id: contact
|
||||
attributes:
|
||||
label: Contact Details
|
||||
description: How can we get in touch with you if we need more info?
|
||||
placeholder: ex. email@example.com, discord username
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: "Checklist"
|
||||
description: |
|
||||
Please make sure you have done the following before submitting your bug report.
|
||||
Bug reports that do not meet these criteria will be closed.
|
||||
Requests that do not meet these criteria will be closed.
|
||||
Links:
|
||||
[**OPEN ISSUES**](https://github.com/meshtastic/Meshtastic-Android/issues)
|
||||
[**CLOSED ISSUES**](https://github.com/meshtastic/Meshtastic-Android/issues?q=is%3Aissue+is%3Aclosed)
|
||||
[Contribution Guidelines](https://github.com/meshtastic/Meshtastic-Android/blob/main/README.md#contributing)
|
||||
options:
|
||||
- label: |
|
||||
I am able to reproduce the bug with the latest version.
|
||||
required: true
|
||||
- label: |
|
||||
I have updated to the latest *Alpha* firmware, and am able to reproduce the bug. Many issues are fixed quickly in alpha before the general beta release.
|
||||
required: true
|
||||
- label: |
|
||||
I made sure that there are no existing **OPEN or CLOSED issues** which I could contribute my information to.
|
||||
required: true
|
||||
- label: |
|
||||
I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise.
|
||||
required: true
|
||||
- label: |
|
||||
This issue contains only one bug.
|
||||
required: true
|
||||
- label: |
|
||||
I have read and understood the **Contribution Guidelines**.
|
||||
required: true
|
||||
- label: |
|
||||
I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: app-version
|
||||
attributes:
|
||||
label: Affected app version
|
||||
description: |
|
||||
In which Meshtastic-Android app version did you encounter the bug?
|
||||
Can be seen on the bottom of the `Settings` screen in the app.
|
||||
placeholder: "x.y.z-channel.x (build) flavor"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: phone-os
|
||||
attributes:
|
||||
label: Affected Android version
|
||||
description: |
|
||||
With what operating system (+ version) did you encounter the bug?
|
||||
placeholder: "Example: Android 14"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: phone-model
|
||||
attributes:
|
||||
label: Affected phone model
|
||||
description: |
|
||||
On what phone did you encounter the bug?
|
||||
placeholder: "Example: Samsung Galaxy S20 / Google Pixel 8"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: hardware-model
|
||||
attributes:
|
||||
label: Affected node model
|
||||
placeholder: "Example: Seeed T1000-E, Heltec v3, etc."
|
||||
description: |
|
||||
On which hardware device (Node) did you encounter the bug?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: firmware-version
|
||||
attributes:
|
||||
label: Affected node firmware version
|
||||
placeholder: "x.x.x"
|
||||
description: "Which Meshtastic firmware version did you encounter the bug?"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce the bug
|
||||
description: |
|
||||
What did you do for the bug to show up?
|
||||
|
||||
If you can't cause the bug to show up again reliably (and hence don't have a proper set of steps to give us), please still try to give as many details as possible on how you think you encountered the bug.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Press on '....'
|
||||
3. Swipe down to '....'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual-behavior
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
description: |
|
||||
Tell us what happens with the steps given above.
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: |
|
||||
Tell us what you expect to happen.
|
||||
|
||||
- type: textarea
|
||||
id: screen-media
|
||||
attributes:
|
||||
label: Screenshots/Screen recordings
|
||||
description: |
|
||||
A picture or video is worth a thousand words.
|
||||
Provide as much context as possible so we know what we are looking at.
|
||||
|
||||
Add screenshots or a screen recording to help explain your problem, provide detailed context to help us know what to look for.
|
||||
GitHub supports uploads of images and (small) videos directly in the text box.
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: |
|
||||
Logs help us to diagnose and reproduce issues, particularly when they are unique to your setup.
|
||||
Depending on the issue, the following logs may be useful:
|
||||
- App logs: This will help with most issues. If possible, provide the relevant output of:
|
||||
`adb logcat -d | grep com.geeksville.mesh`
|
||||
- Mesh logs: UI issues, communication issues etc.
|
||||
- ` App > Settings > Advanced > Debug Panel > Export specific / export all `
|
||||
- Broader Android logs: Potentially useful if the issue goes beyond the app (connections, network etc.)
|
||||
`adb logcat -d`
|
||||
- Firmware logs: Useful for all connection issues with nodes
|
||||
- These are piped to the USB serial port on the node, the most foolproof is to use the 'open serial' button on the web-flasher interface, and then save the output.
|
||||
- The app needs to be connecting to the node via Bluetooth or Network for this to work.
|
||||
render: shell
|
||||
|
||||
- type: textarea
|
||||
id: additional-information
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: |
|
||||
Any other information you'd like to include, for instance that
|
||||
* the affected device is foldable or a TV
|
||||
* you have disabled all animations on your device or otherwise changed system settings
|
||||
* you are using battery optimization or power saving mode
|
||||
* you are using a custom Android ROM or launcher
|
||||
* your ferret chewed your antennas
|
||||
* you are using a VPN
|
||||
* you live in a faraday cage
|
||||
* you dismissed all popups telling you not to do things you shouldn't do without reading them
|
||||
* ...
|
||||
28
.github/PULL_REQUEST_TEMPLATE.md
vendored
28
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -1,28 +0,0 @@
|
|||
## Thank you for sending in a pull request, here's some tips to get started!
|
||||
|
||||
(Please delete all these tips and replace with your text)
|
||||
|
||||
- Before starting on some new big chunk of code, it it is optional but highly recommended to open an issue first to say "Hey, I think this idea X should be implemented and I'm starting work on it. My general plan is Y, any feedback is appreciated." This will allow other devs to potentially save you time by not accidentally duplicating work etc...
|
||||
- Mention "#(issue)" in the description, when applicable
|
||||
- Please do not check in files that don't have real changes
|
||||
- Please do not reformat lines that you didn't have to change the code on
|
||||
- If your other co-developers have comments on your PR please tweak as needed
|
||||
- Do not use any external image service, just paste or drag and drop the image here and it will be uploaded automatically
|
||||
- Please also enable "Allow edits by maintainers".
|
||||
|
||||
<!--
|
||||
If you have screenshots or recordings to display your change, please include them!
|
||||
|
||||
You can use this template for displaying a single screenshot:
|
||||
<img src="" width="300"/>
|
||||
|
||||
or a video recording:
|
||||
<video src="" width="300"></video>
|
||||
|
||||
|
||||
And if you want to display the state before and after a change, you can use this table template:
|
||||
|
||||
| Before | After |
|
||||
|------|-----|
|
||||
| <img src="" width="300"/> | <img src="" width="300"/> |
|
||||
-->
|
||||
40
.github/actions/gradle-setup/action.yml
vendored
40
.github/actions/gradle-setup/action.yml
vendored
|
|
@ -1,40 +0,0 @@
|
|||
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
52
.github/ci-gradle.properties
vendored
|
|
@ -1,52 +0,0 @@
|
|||
#
|
||||
# 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
27
.github/copilot-commit-message-instructions.md
vendored
|
|
@ -1,27 +0,0 @@
|
|||
# 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>
|
||||
6
.github/copilot-instructions.md
vendored
6
.github/copilot-instructions.md
vendored
|
|
@ -1,6 +0,0 @@
|
|||
# Meshtastic Android - GitHub Copilot 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.
|
||||
18
.github/copilot-pull-request-instructions.md
vendored
18
.github/copilot-pull-request-instructions.md
vendored
|
|
@ -1,18 +0,0 @@
|
|||
# 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>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
---
|
||||
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
10
.github/instructions/build-logic.instructions.md
vendored
|
|
@ -1,10 +0,0 @@
|
|||
---
|
||||
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`.
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
---
|
||||
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
20
.github/instructions/kmp-common.instructions.md
vendored
|
|
@ -1,20 +0,0 @@
|
|||
---
|
||||
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`.
|
||||
12
.github/lsp.json
vendored
12
.github/lsp.json
vendored
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"lspServers": {
|
||||
"kotlin": {
|
||||
"command": "kotlin-language-server",
|
||||
"args": [],
|
||||
"fileExtensions": {
|
||||
".kt": "kotlin",
|
||||
".kts": "kotlin"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
.github/meshtastic_logo.png
vendored
BIN
.github/meshtastic_logo.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 89 KiB |
36
.github/release.yml
vendored
36
.github/release.yml
vendored
|
|
@ -1,36 +0,0 @@
|
|||
# .github/release.yml - GitHub Release Notes Configuration
|
||||
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- dependencies
|
||||
- automation
|
||||
- release
|
||||
- repo
|
||||
- skip-changelog
|
||||
- chore
|
||||
- ci
|
||||
- build
|
||||
- testing
|
||||
- test
|
||||
- refactor
|
||||
- documentation
|
||||
- translation
|
||||
authors:
|
||||
- renovate[bot]
|
||||
- dependabot[bot]
|
||||
- github-actions[bot]
|
||||
|
||||
categories:
|
||||
- title: 🏗️ Features
|
||||
labels:
|
||||
- enhancement
|
||||
- feature
|
||||
- title: 🛠️ Fixes
|
||||
labels:
|
||||
- bug
|
||||
- bugfix
|
||||
- fix
|
||||
- title: 📝 Other Changes
|
||||
labels:
|
||||
- '*'
|
||||
96
.github/renovate.json
vendored
96
.github/renovate.json
vendored
|
|
@ -1,96 +0,0 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
":dependencyDashboard",
|
||||
":semanticCommitTypeAll(chore)",
|
||||
":ignoreModulesAndTests",
|
||||
"group:recommended",
|
||||
"replacements:all",
|
||||
"workarounds:all"
|
||||
],
|
||||
"commitMessageTopic": "{{depName}}",
|
||||
"labels": [
|
||||
"dependencies"
|
||||
],
|
||||
"git-submodules": {
|
||||
"enabled": true
|
||||
},
|
||||
"bundler": {
|
||||
"enabled": true
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Automerge non-major updates for stable versions",
|
||||
"matchUpdateTypes": [
|
||||
"minor",
|
||||
"patch",
|
||||
"pin",
|
||||
"digest"
|
||||
],
|
||||
"matchCurrentVersion": "!/^0/",
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Automerge patch updates for unstable (0.x) versions",
|
||||
"matchUpdateTypes": [
|
||||
"patch",
|
||||
"pin",
|
||||
"digest"
|
||||
],
|
||||
"matchCurrentVersion": "/^0/",
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Automerge pins and digests regardless of version",
|
||||
"matchUpdateTypes": [
|
||||
"pin",
|
||||
"digest"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Meshtastic Protobufs changelog link",
|
||||
"matchPackageNames": [
|
||||
"https://github.com/meshtastic/protobufs.git"
|
||||
],
|
||||
"changelogUrl": "https://github.com/meshtastic/protobufs/compare/{{currentDigest}}...{{newDigest}}",
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "Group CMP and the androidx.compose artifacts that track it so Renovate bumps them together (see PR #5180)",
|
||||
"groupName": "compose-multiplatform",
|
||||
"matchPackageNames": [
|
||||
"/^org\\.jetbrains\\.compose/",
|
||||
"androidx.compose.runtime:runtime-tracing",
|
||||
"androidx.compose.ui:ui-test-manifest"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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/",
|
||||
"/^androidx\\.lifecycle/",
|
||||
"/^androidx\\.navigation/",
|
||||
"/^androidx\\.datastore/",
|
||||
"/^androidx\\.compose\\.material3\\.adaptive/",
|
||||
"/^androidx\\.compose\\.material3:material3-adaptive-navigation-suite$/"
|
||||
],
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"description": "Disable automerge for major updates (safety net)",
|
||||
"matchUpdateTypes": [
|
||||
"major"
|
||||
],
|
||||
"automerge": false
|
||||
}
|
||||
]
|
||||
}
|
||||
17
.github/workflows/android.yml
vendored
Normal file
17
.github/workflows/android.yml
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
name: Android CI
|
||||
# from https://medium.com/@wkrzywiec/github-actions-for-android-first-approach-f616c24aa0f9
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: set up JDK 1.8
|
||||
uses: actions/setup-java@master
|
||||
with:
|
||||
java-version: 1.8
|
||||
- name: Unit tests
|
||||
run: bash ./gradlew test --stacktrace
|
||||
166
.github/workflows/create-or-promote-release.yml
vendored
166
.github/workflows/create-or-promote-release.yml
vendored
|
|
@ -1,166 +0,0 @@
|
|||
name: Create or Promote Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
base_version:
|
||||
description: 'Base version for the release (e.g., 2.3.0)'
|
||||
required: true
|
||||
channel:
|
||||
description: 'The channel to create a release for or promote to'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- internal
|
||||
- closed
|
||||
- open
|
||||
- production
|
||||
dry_run:
|
||||
description: 'If true, calculates the tag but does not push it or start the release'
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
build_desktop:
|
||||
description: 'Whether to build the desktop distribution'
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
jobs:
|
||||
determine-tags:
|
||||
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 }}
|
||||
final_tag: ${{ steps.calculate_tags.outputs.final_tag }}
|
||||
from_channel: ${{ steps.calculate_tags.outputs.from_channel }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.CROWDIN_GITHUB_TOKEN }}
|
||||
|
||||
- name: Calculate tags
|
||||
id: calculate_tags
|
||||
run: |
|
||||
BASE_VERSION="${{ inputs.base_version }}"
|
||||
CHANNEL="${{ inputs.channel }}"
|
||||
|
||||
if [[ "$CHANNEL" == "internal" ]]; then
|
||||
# This is a new build, create a new internal tag
|
||||
LATEST_TAG=$(git tag --list "v${BASE_VERSION}-internal.*" --sort=-v:refname | head -n 1)
|
||||
|
||||
if [ -z "$LATEST_TAG" ]; then
|
||||
INCREMENT=1
|
||||
else
|
||||
INCREMENT=$(echo "$LATEST_TAG" | sed -n "s/.*-internal\.\([0-9]*\)/\1/p" | awk '{print $1+1}')
|
||||
fi
|
||||
|
||||
NEW_TAG="v${BASE_VERSION}-internal.${INCREMENT}"
|
||||
echo "Calculated new tag: $NEW_TAG"
|
||||
echo "tag_to_process=$NEW_TAG" >> $GITHUB_OUTPUT
|
||||
echo "release_name=$NEW_TAG" >> $GITHUB_OUTPUT
|
||||
echo "final_tag=$NEW_TAG" >> $GITHUB_OUTPUT
|
||||
else
|
||||
# This is a promotion, find the latest tag from the previous channel to promote
|
||||
FROM_CHANNEL="internal"
|
||||
if [[ "$CHANNEL" == "open" ]]; then
|
||||
FROM_CHANNEL="closed"
|
||||
elif [[ "$CHANNEL" == "production" ]]; then
|
||||
FROM_CHANNEL="open"
|
||||
fi
|
||||
|
||||
LATEST_TAG_TO_PROMOTE=$(git tag --list "v${BASE_VERSION}-${FROM_CHANNEL}.*" --sort=-v:refname | head -n 1)
|
||||
|
||||
if [ -z "$LATEST_TAG_TO_PROMOTE" ]; then
|
||||
echo "::error::No ${FROM_CHANNEL} release found for base version ${BASE_VERSION} to promote."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Found latest ${FROM_CHANNEL} tag to promote: $LATEST_TAG_TO_PROMOTE"
|
||||
|
||||
# Calculate the increment for the TARGET channel
|
||||
if [[ "$CHANNEL" != "production" ]]; then
|
||||
LATEST_CHANNEL_TAG=$(git tag --list "v${BASE_VERSION}-${CHANNEL}.*" --sort=-v:refname | head -n 1)
|
||||
|
||||
if [ -z "$LATEST_CHANNEL_TAG" ]; then
|
||||
INCREMENT=1
|
||||
else
|
||||
INCREMENT=$(echo "$LATEST_CHANNEL_TAG" | sed -n "s/.*-${CHANNEL}\.\([0-9]*\)/\1/p" | awk '{print $1+1}')
|
||||
fi
|
||||
|
||||
NEW_TAG="v${BASE_VERSION}-${CHANNEL}.${INCREMENT}"
|
||||
else
|
||||
# Production is special, it has no increment
|
||||
NEW_TAG="v${BASE_VERSION}"
|
||||
fi
|
||||
|
||||
echo "New release name will be: $NEW_TAG"
|
||||
echo "Final tag will be: $NEW_TAG"
|
||||
echo "from_channel=${FROM_CHANNEL}" >> $GITHUB_OUTPUT
|
||||
echo "tag_to_process=${LATEST_TAG_TO_PROMOTE}" >> $GITHUB_OUTPUT
|
||||
echo "release_name=${NEW_TAG}" >> $GITHUB_OUTPUT
|
||||
echo "final_tag=${NEW_TAG}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Create and Push Release Tag
|
||||
if: ${{ !inputs.dry_run && inputs.channel == 'internal' }}
|
||||
env:
|
||||
FINAL_TAG: ${{ steps.calculate_tags.outputs.final_tag }}
|
||||
run: |
|
||||
echo "Tagging and pushing release: $FINAL_TAG"
|
||||
git tag "$FINAL_TAG"
|
||||
git push origin "$FINAL_TAG"
|
||||
shell: bash
|
||||
|
||||
call-release-workflow:
|
||||
if: ${{ !inputs.dry_run && inputs.channel == 'internal' }}
|
||||
needs: determine-tags
|
||||
uses: ./.github/workflows/release.yml
|
||||
with:
|
||||
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:
|
||||
if: ${{ !inputs.dry_run && inputs.channel != 'internal' }}
|
||||
needs: determine-tags
|
||||
uses: ./.github/workflows/promote.yml
|
||||
with:
|
||||
tag_name: ${{ needs.determine-tags.outputs.tag_to_process }}
|
||||
release_name: ${{ needs.determine-tags.outputs.release_name }}
|
||||
final_tag: ${{ needs.determine-tags.outputs.final_tag }}
|
||||
channel: ${{ inputs.channel }}
|
||||
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
|
||||
29
.github/workflows/dependency-submission.yml
vendored
29
.github/workflows/dependency-submission.yml
vendored
|
|
@ -1,29 +0,0 @@
|
|||
name: Dependency Submission
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'main' ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
dependency-submission:
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
token: ${{ github.token }}
|
||||
|
||||
- name: Generate and submit dependency graph
|
||||
uses: gradle/actions/dependency-submission@v6
|
||||
with:
|
||||
build-scan-publish: true
|
||||
build-scan-terms-of-use-url: "https://gradle.com/terms-of-service"
|
||||
build-scan-terms-of-use-agree: "yes"
|
||||
83
.github/workflows/docs.yml
vendored
83
.github/workflows/docs.yml
vendored
|
|
@ -1,83 +0,0 @@
|
|||
# This workflow builds and deploys the Dokka documentation to GitHub Pages.
|
||||
|
||||
name: Deploy Documentation
|
||||
|
||||
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:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'The branch, tag or SHA to checkout'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
# Allow this workflow to be called from other workflows
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'The branch, tag or SHA to checkout'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment; cancel queued runs since only the latest
|
||||
# main state matters for documentation.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-docs:
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
ref: ${{ inputs.ref || '' }}
|
||||
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
with:
|
||||
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
|
||||
- name: Build Dokka HTML documentation
|
||||
run: ./gradlew dokkaGeneratePublicationHtml
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: build/dokka/html
|
||||
|
||||
deploy:
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-24.04-arm
|
||||
needs: build-docs
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v5
|
||||
26
.github/workflows/main-check.yml
vendored
26
.github/workflows/main-check.yml
vendored
|
|
@ -1,26 +0,0 @@
|
|||
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
|
||||
71
.github/workflows/main-push-changelog.yml
vendored
71
.github/workflows/main-push-changelog.yml
vendored
|
|
@ -1,71 +0,0 @@
|
|||
name: Main Push Changelog
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: main-push-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
main-push-changelog:
|
||||
name: Generate main push changelog
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Determine last tag
|
||||
id: last_prod_tag
|
||||
run: |
|
||||
TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
||||
echo "Found last tag: $TAG"
|
||||
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Generate changelog from last tag to current
|
||||
if: steps.last_prod_tag.outputs.tag != ''
|
||||
uses: mikepenz/release-changelog-builder-action@v6
|
||||
id: changelog
|
||||
with:
|
||||
configuration: .github/release.yml
|
||||
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 }}
|
||||
|
||||
- name: Upload changelog artifact
|
||||
if: steps.last_prod_tag.outputs.tag != ''
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: main-push-changelog
|
||||
path: main-push-changelog.md
|
||||
|
||||
- name: Print main push summary
|
||||
env:
|
||||
LAST_TAG: ${{ steps.last_prod_tag.outputs.tag }}
|
||||
run: |
|
||||
echo "Pushed to main"
|
||||
echo "SHA: $GITHUB_SHA"
|
||||
echo "Actor: $GITHUB_ACTOR"
|
||||
echo "Ref: $GITHUB_REF"
|
||||
echo ""
|
||||
if [ "$LAST_TAG" != "" ]; then
|
||||
echo "Changelog since last tag ($LAST_TAG)":
|
||||
echo "----------------------------------------"
|
||||
cat main-push-changelog.md
|
||||
else
|
||||
echo "No tag found. Skipping changelog generation."
|
||||
fi
|
||||
38
.github/workflows/merge-queue.yml
vendored
38
.github/workflows/merge-queue.yml
vendored
|
|
@ -1,38 +0,0 @@
|
|||
name: Android CI (Merge Queue)
|
||||
|
||||
on:
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: build-mq-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
android-check:
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
uses: ./.github/workflows/reusable-check.yml
|
||||
with:
|
||||
run_lint: true
|
||||
run_unit_tests: true
|
||||
upload_artifacts: false
|
||||
secrets: inherit
|
||||
|
||||
check-workflow-status:
|
||||
name: Check Workflow Status
|
||||
runs-on: ubuntu-24.04-arm
|
||||
permissions: {}
|
||||
needs:
|
||||
- android-check
|
||||
if: always()
|
||||
steps:
|
||||
- name: Check Workflow Status
|
||||
run: |
|
||||
if [[ "${{ needs.android-check.result }}" == "failure" || "${{ needs.android-check.result }}" == "cancelled" ]]; then
|
||||
echo "::error::Android Check failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "All jobs passed successfully"
|
||||
204
.github/workflows/models_issue_triage.yml
vendored
204
.github/workflows/models_issue_triage.yml
vendored
|
|
@ -1,204 +0,0 @@
|
|||
name: Issue Triage (Models)
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
models: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
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
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
- name: Detect spam or low-quality content
|
||||
uses: actions/ai-inference@v2
|
||||
id: quality
|
||||
continue-on-error: true
|
||||
with:
|
||||
max-tokens: 20
|
||||
prompt: |
|
||||
Is this GitHub issue spam, AI-generated slop, or low quality?
|
||||
|
||||
Title: ${{ github.event.issue.title }}
|
||||
Body: ${{ github.event.issue.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.
|
||||
model: openai/gpt-4o-mini
|
||||
|
||||
- name: Apply quality label if needed
|
||||
if: steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok'
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
QUALITY_LABEL: ${{ steps.quality.outputs.response }}
|
||||
with:
|
||||
script: |
|
||||
const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase();
|
||||
const labelMeta = {
|
||||
'spam': { color: 'd73a4a', description: 'Possible spam' },
|
||||
'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' },
|
||||
'needs-review': { color: 'f9d0c4', description: 'Needs human review' },
|
||||
};
|
||||
const meta = labelMeta[label];
|
||||
if (!meta) return;
|
||||
|
||||
// Ensure label exists
|
||||
try {
|
||||
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
|
||||
}
|
||||
|
||||
// Apply label
|
||||
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] });
|
||||
|
||||
// Set output to skip remaining steps
|
||||
core.setOutput('is_spam', 'true');
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Step 2: Duplicate detection - only if not spam
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
- name: Detect duplicate issues
|
||||
if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == ''
|
||||
uses: pelikhan/action-genai-issue-dedup@bdb3b5d9451c1090ffcdf123d7447a5e7c7a2528 # v0.0.19
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Step 3: Completeness check + auto-labeling (combined into one AI call)
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
- name: Determine if completeness check should be skipped
|
||||
if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == ''
|
||||
uses: actions/github-script@v9
|
||||
id: check-skip
|
||||
with:
|
||||
script: |
|
||||
const title = (context.payload.issue.title || '').toLowerCase();
|
||||
const labels = (context.payload.issue.labels || []).map(label => label.name);
|
||||
const hasFeatureRequest = title.includes('feature request');
|
||||
const hasEnhancement = labels.includes('enhancement');
|
||||
const shouldSkip = hasFeatureRequest && hasEnhancement;
|
||||
core.setOutput('should_skip', shouldSkip ? 'true' : 'false');
|
||||
|
||||
- name: Analyze issue completeness and determine labels
|
||||
if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true'
|
||||
uses: actions/ai-inference@v2
|
||||
id: analysis
|
||||
continue-on-error: true
|
||||
with:
|
||||
prompt: |
|
||||
Analyze this GitHub issue for the Meshtastic Android app and determine if it needs labels.
|
||||
|
||||
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:
|
||||
|
||||
Android app debug logs:
|
||||
- Open the Meshtastic app, go to Settings > Debug > Save Logs
|
||||
- Reproduce the problem, then share/attach the exported log file
|
||||
|
||||
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: 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:
|
||||
{
|
||||
"complete": true|false,
|
||||
"comment": "Your helpful comment requesting missing info, or empty string if complete",
|
||||
"label": "needs-logs" | "needs-info" | "none"
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
Title: ${{ github.event.issue.title }}
|
||||
Body: ${{ github.event.issue.body }}
|
||||
system-prompt: You are a helpful assistant that triages GitHub issues. Be conservative with labels.
|
||||
model: openai/gpt-4o-mini
|
||||
|
||||
- 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@v9
|
||||
id: process
|
||||
env:
|
||||
AI_RESPONSE: ${{ steps.analysis.outputs.response }}
|
||||
with:
|
||||
script: |
|
||||
const raw = (process.env.AI_RESPONSE || '').trim();
|
||||
|
||||
let complete = false;
|
||||
let comment = '';
|
||||
let label = 'none';
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
complete = !!parsed.complete;
|
||||
comment = (parsed.comment ?? '').toString().trim();
|
||||
label = (parsed.label ?? 'none').toString().trim().toLowerCase();
|
||||
} catch {
|
||||
// If JSON parse fails, treat as incomplete with raw response as comment
|
||||
complete = false;
|
||||
comment = raw;
|
||||
label = 'none';
|
||||
}
|
||||
|
||||
// Validate label
|
||||
const allowedLabels = new Set(['needs-logs', 'needs-info', 'none']);
|
||||
if (!allowedLabels.has(label)) label = 'none';
|
||||
|
||||
core.setOutput('should_comment', (!complete && comment.length > 0) ? 'true' : 'false');
|
||||
core.setOutput('comment_body', comment);
|
||||
core.setOutput('label', label);
|
||||
|
||||
- name: Apply triage label
|
||||
if: steps.process.outputs.label != '' && steps.process.outputs.label != 'none'
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
LABEL_NAME: ${{ steps.process.outputs.label }}
|
||||
with:
|
||||
script: |
|
||||
const label = process.env.LABEL_NAME;
|
||||
const labelMeta = {
|
||||
'needs-logs': { color: 'cfd3d7', description: 'Device logs requested for triage' },
|
||||
'needs-info': { color: 'f9d0c4', description: 'More information requested for triage' },
|
||||
};
|
||||
const meta = labelMeta[label];
|
||||
if (!meta) return;
|
||||
|
||||
// Ensure label exists
|
||||
try {
|
||||
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
|
||||
}
|
||||
|
||||
// Apply label
|
||||
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] });
|
||||
|
||||
- name: Comment on issue
|
||||
if: steps.process.outputs.should_comment == 'true'
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
COMMENT_BODY: ${{ steps.process.outputs.comment_body }}
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.issue.number,
|
||||
body: process.env.COMMENT_BODY
|
||||
});
|
||||
144
.github/workflows/models_pr_triage.yml
vendored
144
.github/workflows/models_pr_triage.yml
vendored
|
|
@ -1,144 +0,0 @@
|
|||
name: PR Triage (Models)
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
models: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
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@v9
|
||||
id: check-labels
|
||||
with:
|
||||
script: |
|
||||
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));
|
||||
const hasTypeLabel = prLabels.some(l => typeLabels.has(l));
|
||||
|
||||
core.setOutput('skip_all', shouldSkipAll ? 'true' : 'false');
|
||||
core.setOutput('has_type_label', hasTypeLabel ? 'true' : 'false');
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Step 2: Quality check (spam/AI-slop detection)
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
- name: Detect spam or low-quality content
|
||||
if: steps.check-labels.outputs.skip_all != 'true'
|
||||
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: ${{ 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.
|
||||
model: openai/gpt-4o-mini
|
||||
|
||||
- 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@v9
|
||||
id: quality-label
|
||||
env:
|
||||
QUALITY_LABEL: ${{ steps.quality.outputs.response }}
|
||||
with:
|
||||
script: |
|
||||
const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase();
|
||||
const labelMeta = {
|
||||
'spam': { color: 'd73a4a', description: 'Possible spam' },
|
||||
'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' },
|
||||
'needs-review': { color: 'f9d0c4', description: 'Needs human review' },
|
||||
};
|
||||
const meta = labelMeta[label];
|
||||
if (!meta) return;
|
||||
|
||||
// Ensure label exists
|
||||
try {
|
||||
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
|
||||
}
|
||||
|
||||
// Apply label
|
||||
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] });
|
||||
|
||||
core.setOutput('is_spam', 'true');
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# 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 for the Meshtastic Android app into exactly one category.
|
||||
|
||||
Return exactly one of: bugfix, enhancement, refactor
|
||||
|
||||
Use bugfix if it fixes a bug, crash, or incorrect behavior.
|
||||
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: ${{ 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@v9
|
||||
env:
|
||||
TYPE_LABEL: ${{ steps.classify.outputs.response }}
|
||||
with:
|
||||
script: |
|
||||
const label = (process.env.TYPE_LABEL || '').trim().toLowerCase();
|
||||
const labelMeta = {
|
||||
'bugfix': { color: 'd73a4a', description: 'Bug fix' },
|
||||
'enhancement': { color: 'a2eeef', description: 'New feature or enhancement' },
|
||||
'refactor': { color: 'c5def5', description: 'Code restructuring without behavior change' },
|
||||
};
|
||||
const meta = labelMeta[label];
|
||||
if (!meta) return;
|
||||
|
||||
// Ensure label exists
|
||||
try {
|
||||
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description });
|
||||
}
|
||||
|
||||
// Apply label
|
||||
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] });
|
||||
31
.github/workflows/moderate.yml
vendored
31
.github/workflows/moderate.yml
vendored
|
|
@ -1,31 +0,0 @@
|
|||
name: AI Moderator
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
spam-detection:
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
runs-on: ubuntu-24.04-arm
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
models: read
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: github/ai-moderator@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
spam-label: 'spam'
|
||||
ai-label: 'ai-generated'
|
||||
minimize-detected-comments: true
|
||||
# Built-in prompt configuration (all enabled by default)
|
||||
enable-spam-detection: true
|
||||
enable-link-spam-detection: true
|
||||
enable-ai-detection: true
|
||||
# custom-prompt-path: '.github/prompts/my-custom.prompt.yml' # Optional
|
||||
78
.github/workflows/post-release-cleanup.yml
vendored
78
.github/workflows/post-release-cleanup.yml
vendored
|
|
@ -1,78 +0,0 @@
|
|||
name: Post-Release Cleanup
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
base_version:
|
||||
description: 'The base version to clean up (e.g., 2.3.0)'
|
||||
required: true
|
||||
type: string
|
||||
confirm_deletion:
|
||||
description: 'WARNING: This is a destructive action. Set to true to perform deletion. Defaults to a dry run.'
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
cleanup_prereleases:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
environment: Release
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cleanup pre-releases and their tags
|
||||
id: cleanup_releases
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
BASE_VERSION="${{ github.event.inputs.base_version }}"
|
||||
TAG_PREFIX="v${BASE_VERSION}-"
|
||||
echo "Searching for pre-releases with tag prefix '$TAG_PREFIX'."
|
||||
RELEASES_TO_DELETE=$(gh release list --json tagName,isPrerelease --limit 100 | jq -r --arg prefix "$TAG_PREFIX" '.[] | select(.isPrerelease == true and .tagName != null and (.tagName | startswith($prefix))) | .tagName')
|
||||
|
||||
if [ -z "$RELEASES_TO_DELETE" ]; then
|
||||
echo "No pre-releases found for base version $BASE_VERSION."
|
||||
else
|
||||
if [[ "${{ github.event.inputs.confirm_deletion }}" == "true" ]]; then
|
||||
echo "!!! DELETING RELEASES AND TAGS !!!"
|
||||
echo "The following pre-releases and their tags will be deleted:"
|
||||
echo "$RELEASES_TO_DELETE"
|
||||
echo "$RELEASES_TO_DELETE" | xargs -n 1 gh release delete --cleanup-tag --yes
|
||||
else
|
||||
echo "DRY RUN: The following pre-releases and their tags would be deleted:"
|
||||
echo "$RELEASES_TO_DELETE"
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Cleanup dangling pre-release tags
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
BASE_VERSION="${{ github.event.inputs.base_version }}"
|
||||
TAG_PREFIX="v${BASE_VERSION}-"
|
||||
echo "Searching for any remaining remote pre-release tags with prefix '$TAG_PREFIX'."
|
||||
|
||||
# This finds all remote tags matching the pattern. Some may have been deleted in the previous step.
|
||||
TAGS_TO_DELETE=$(git ls-remote --tags origin "refs/tags/${TAG_PREFIX}*" | awk '{print $2}' | sed 's|refs/tags/||')
|
||||
|
||||
if [ -z "$TAGS_TO_DELETE" ]; then
|
||||
echo "No dangling pre-release tags found."
|
||||
else
|
||||
if [[ "${{ github.event.inputs.confirm_deletion }}" == "true" ]]; then
|
||||
echo "!!! DELETING DANGLING TAGS !!!"
|
||||
echo "The following pre-release tags will be deleted:"
|
||||
# We pipe to xargs which will run the command for each tag.
|
||||
# If a tag was already deleted by the previous 'release delete' step, this will fail for that tag.
|
||||
# We add '|| true' to ignore any errors and ensure the workflow doesn't fail.
|
||||
echo "$TAGS_TO_DELETE" | xargs -n 1 -I {} sh -c 'git push --delete origin {} || true'
|
||||
else
|
||||
echo "DRY RUN: The following dangling pre-release tags would be deleted:"
|
||||
echo "$TAGS_TO_DELETE"
|
||||
fi
|
||||
fi
|
||||
37
.github/workflows/pr_enforce_labels.yml
vendored
37
.github/workflows/pr_enforce_labels.yml
vendored
|
|
@ -1,37 +0,0 @@
|
|||
name: Check PR Labels
|
||||
|
||||
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:
|
||||
# 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@v9
|
||||
with:
|
||||
script: |
|
||||
// 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));
|
||||
if (!hasRequiredLabel) {
|
||||
core.setFailed(`PR must have at least one of the following labels before it can be merged: ${requiredLabels.join(', ')}.`);
|
||||
}
|
||||
190
.github/workflows/promote.yml
vendored
190
.github/workflows/promote.yml
vendored
|
|
@ -1,190 +0,0 @@
|
|||
name: Promote Release
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
base_version:
|
||||
description: 'The base version for the release (e.g., 2.3.0)'
|
||||
required: true
|
||||
type: string
|
||||
tag_name:
|
||||
description: 'The tag that triggered the release'
|
||||
required: true
|
||||
type: string
|
||||
release_name:
|
||||
description: 'The desired name for the GitHub release'
|
||||
required: true
|
||||
type: string
|
||||
final_tag:
|
||||
description: 'The final tag for the release'
|
||||
required: true
|
||||
type: string
|
||||
commit_sha:
|
||||
description: 'The commit SHA to tag'
|
||||
required: false
|
||||
type: string
|
||||
channel:
|
||||
description: 'The channel to promote to'
|
||||
required: true
|
||||
type: string
|
||||
from_channel:
|
||||
description: 'The channel to promote from'
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
GSERVICES:
|
||||
required: true
|
||||
KEYSTORE:
|
||||
required: true
|
||||
KEYSTORE_FILENAME:
|
||||
required: true
|
||||
KEYSTORE_PROPERTIES:
|
||||
required: true
|
||||
DATADOG_APPLICATION_ID:
|
||||
required: true
|
||||
DATADOG_CLIENT_TOKEN:
|
||||
required: true
|
||||
GOOGLE_MAPS_API_KEY:
|
||||
required: true
|
||||
GOOGLE_PLAY_JSON_KEY:
|
||||
required: true
|
||||
GRADLE_ENCRYPTION_KEY:
|
||||
required: true
|
||||
DISCORD_WEBHOOK_ANDROID:
|
||||
required: false
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ inputs.tag_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
jobs:
|
||||
prepare-build-info:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
outputs:
|
||||
APP_VERSION_NAME: ${{ steps.prep_version.outputs.APP_VERSION_NAME }}
|
||||
APP_VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.commit_sha || inputs.tag_name }}
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
|
||||
- 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
|
||||
run: |
|
||||
OFFSET=$(grep '^VERSION_CODE_OFFSET=' config.properties | cut -d'=' -f2)
|
||||
echo "VERSION_CODE_OFFSET=$OFFSET" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Calculate Version Code from Git Commit Count
|
||||
id: calculate_version_code
|
||||
run: |
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
OFFSET=${{ steps.get_version_code_offset.outputs.VERSION_CODE_OFFSET }}
|
||||
VERSION_CODE=$((COMMIT_COUNT + OFFSET))
|
||||
echo "versionCode=$VERSION_CODE" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
promote-release:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
environment: Release
|
||||
needs: [ prepare-build-info ]
|
||||
steps:
|
||||
- name: Promote to next channel
|
||||
uses: kevin-david/promote-play-release@v1.2.0
|
||||
with:
|
||||
service-account-json-raw: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
|
||||
package-name: 'com.geeksville.mesh'
|
||||
from-track: ${{ inputs.from_channel == 'closed' && 'NewAlpha' || (inputs.from_channel == 'open' && 'beta' || 'internal') }}
|
||||
to-track: ${{ inputs.channel == 'closed' && 'NewAlpha' || (inputs.channel == 'open' && 'beta' || 'production') }}
|
||||
user-fraction: ${{ (inputs.channel == 'production' && '0.1') || (inputs.channel == 'open' && '0.5') || '1.0' }}
|
||||
|
||||
update-github-release:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
needs: [ prepare-build-info, promote-release ]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.commit_sha || inputs.tag_name }}
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Push Git Tag on Success
|
||||
if: ${{ inputs.commit_sha != '' }}
|
||||
run: |
|
||||
git tag ${{ inputs.final_tag }} ${{ inputs.commit_sha }}
|
||||
git push origin ${{ inputs.final_tag }}
|
||||
|
||||
- name: Update GitHub Release with gh CLI
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
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
|
||||
if: ${{ inputs.channel != 'internal' }}
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_ANDROID }}
|
||||
VERSION: ${{ inputs.final_tag }}
|
||||
CHANNEL: ${{ inputs.channel }}
|
||||
run: |
|
||||
if [[ -z "$DISCORD_WEBHOOK" ]]; then
|
||||
echo "No Discord webhook provided. Skipping notification."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Determine Track Name for Display
|
||||
if [ "$CHANNEL" == "closed" ]; then TRACK="Alpha (Closed)"; fi
|
||||
if [ "$CHANNEL" == "open" ]; then TRACK="Beta (Open)"; fi
|
||||
if [ "$CHANNEL" == "production" ]; then TRACK="Production"; fi
|
||||
|
||||
# Construct JSON Payload
|
||||
PAYLOAD=$(cat <<EOF
|
||||
{
|
||||
"content": null,
|
||||
"embeds": [
|
||||
{
|
||||
"title": "🚀 New Android Release: $VERSION",
|
||||
"description": "A new build has been promoted to the **$TRACK** track.",
|
||||
"color": 5763719,
|
||||
"fields": [
|
||||
{
|
||||
"name": "Track",
|
||||
"value": "$TRACK",
|
||||
"inline": true
|
||||
},
|
||||
{
|
||||
"name": "Version",
|
||||
"value": "$VERSION",
|
||||
"inline": true
|
||||
}
|
||||
],
|
||||
"url": "https://github.com/meshtastic/Meshtastic-Android/releases/tag/$VERSION"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
curl -H "Content-Type: application/json" -d "$PAYLOAD" "$DISCORD_WEBHOOK"
|
||||
51
.github/workflows/publish-core.yml
vendored
51
.github/workflows/publish-core.yml
vendored
|
|
@ -1,51 +0,0 @@
|
|||
name: Publish Core Libraries
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_suffix:
|
||||
description: 'Version suffix (e.g. -alpha01, -SNAPSHOT)'
|
||||
required: false
|
||||
default: '-SNAPSHOT'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
with:
|
||||
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 [[ "$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}${VERSION_SUFFIX}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Publish to GitHub Packages
|
||||
run: ./gradlew :core:api:publish :core:model:publish :core:proto:publish
|
||||
env:
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
67
.github/workflows/pull-request-target.yml
vendored
67
.github/workflows/pull-request-target.yml
vendored
|
|
@ -1,67 +0,0 @@
|
|||
name: "Pull Request Labeler"
|
||||
on:
|
||||
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-24.04-arm
|
||||
steps:
|
||||
- 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.');
|
||||
}
|
||||
134
.github/workflows/pull-request.yml
vendored
134
.github/workflows/pull-request.yml
vendored
|
|
@ -1,134 +0,0 @@
|
|||
name: Pull Request CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
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-24.04-arm
|
||||
outputs:
|
||||
android: ${{ steps.filter.outputs.android }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dorny/paths-filter@v4
|
||||
id: filter
|
||||
with:
|
||||
token: ''
|
||||
filters: |
|
||||
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'
|
||||
- 'settings.gradle.kts'
|
||||
- 'test.gradle.kts'
|
||||
|
||||
# 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.android == 'true'
|
||||
uses: ./.github/workflows/reusable-check.yml
|
||||
with:
|
||||
run_lint: true
|
||||
run_unit_tests: true
|
||||
run_coverage: false
|
||||
run_desktop_builds: false
|
||||
upload_artifacts: true
|
||||
secrets: inherit
|
||||
|
||||
# 3. WORKFLOW STATUS: Ensures required checks are satisfied
|
||||
check-workflow-status:
|
||||
name: Check Workflow Status
|
||||
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.verify-check-changes-filter.result }}" == "failure" || "${{ needs.verify-check-changes-filter.result }}" == "cancelled" ]]; then
|
||||
echo "::error::check-changes filter verification failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 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."
|
||||
353
.github/workflows/release.yml
vendored
353
.github/workflows/release.yml
vendored
|
|
@ -1,353 +0,0 @@
|
|||
name: Make Release
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
base_version:
|
||||
description: 'The base version for the release (e.g., 2.3.0)'
|
||||
required: true
|
||||
type: string
|
||||
tag_name:
|
||||
description: 'The tag that triggered the release'
|
||||
required: true
|
||||
type: string
|
||||
commit_sha:
|
||||
description: 'The commit SHA to build and tag'
|
||||
required: false
|
||||
type: string
|
||||
channel:
|
||||
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
|
||||
KEYSTORE:
|
||||
required: true
|
||||
KEYSTORE_FILENAME:
|
||||
required: true
|
||||
KEYSTORE_PROPERTIES:
|
||||
required: true
|
||||
DATADOG_APPLICATION_ID:
|
||||
required: true
|
||||
DATADOG_CLIENT_TOKEN:
|
||||
required: true
|
||||
GOOGLE_MAPS_API_KEY:
|
||||
required: true
|
||||
GOOGLE_PLAY_JSON_KEY:
|
||||
required: true
|
||||
GRADLE_ENCRYPTION_KEY:
|
||||
required: true
|
||||
GRADLE_CACHE_URL:
|
||||
required: false
|
||||
GRADLE_CACHE_USERNAME:
|
||||
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 }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
jobs:
|
||||
prepare-build-info:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
outputs:
|
||||
APP_VERSION_NAME: ${{ steps.prep_version.outputs.APP_VERSION_NAME }}
|
||||
APP_VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }}
|
||||
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: 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
|
||||
run: |
|
||||
OFFSET=$(grep '^VERSION_CODE_OFFSET=' config.properties | cut -d'=' -f2)
|
||||
echo "VERSION_CODE_OFFSET=$OFFSET" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Calculate Version Code from Git Commit Count
|
||||
id: calculate_version_code
|
||||
run: |
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
OFFSET=${{ steps.get_version_code_offset.outputs.VERSION_CODE_OFFSET }}
|
||||
VERSION_CODE=$((COMMIT_COUNT + OFFSET))
|
||||
echo "versionCode=$VERSION_CODE" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
release-google:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [prepare-build-info]
|
||||
environment: Release
|
||||
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: Load secrets
|
||||
env:
|
||||
GSERVICES: ${{ secrets.GSERVICES }}
|
||||
KEYSTORE: ${{ secrets.KEYSTORE }}
|
||||
KEYSTORE_FILENAME: ${{ secrets.KEYSTORE_FILENAME }}
|
||||
KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }}
|
||||
DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }}
|
||||
DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }}
|
||||
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
|
||||
GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
|
||||
run: |
|
||||
rm -f ./app/google-services.json
|
||||
echo $GSERVICES > ./app/google-services.json
|
||||
echo $KEYSTORE | base64 -di > ./app/$KEYSTORE_FILENAME
|
||||
echo "$KEYSTORE_PROPERTIES" > ./keystore.properties
|
||||
echo "datadogApplicationId=$DATADOG_APPLICATION_ID" >> ./secrets.properties
|
||||
echo "datadogClientToken=$DATADOG_CLIENT_TOKEN" >> ./secrets.properties
|
||||
echo "MAPS_API_KEY=$GOOGLE_MAPS_API_KEY" >> ./secrets.properties
|
||||
echo "$GOOGLE_PLAY_JSON_KEY" > ./fastlane/play-store-credentials.json
|
||||
|
||||
- name: Setup Fastlane
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.4.9'
|
||||
bundler-cache: true
|
||||
|
||||
- name: Build and Deploy Google Play to Internal Track with Fastlane
|
||||
env:
|
||||
VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
|
||||
VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
|
||||
run: bundle exec fastlane internal
|
||||
|
||||
- name: List outputs
|
||||
run: ls -R app/build/outputs/
|
||||
|
||||
- name: Upload Google AAB artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: google-aab
|
||||
path: app/build/outputs/bundle/googleRelease/app-google-release.aab
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload Google APK artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: google-apk
|
||||
path: app/build/outputs/apk/google/release/*.apk
|
||||
retention-days: 1
|
||||
|
||||
- name: Attest Google AAB provenance
|
||||
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: success()
|
||||
uses: actions/attest-build-provenance@v4
|
||||
with:
|
||||
subject-path: app/build/outputs/apk/google/release/*.apk
|
||||
|
||||
release-fdroid:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [prepare-build-info]
|
||||
environment: Release
|
||||
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: Load secrets
|
||||
env:
|
||||
KEYSTORE: ${{ secrets.KEYSTORE }}
|
||||
KEYSTORE_FILENAME: ${{ secrets.KEYSTORE_FILENAME }}
|
||||
KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }}
|
||||
run: |
|
||||
echo $KEYSTORE | base64 -di > ./app/$KEYSTORE_FILENAME
|
||||
echo "$KEYSTORE_PROPERTIES" > ./keystore.properties
|
||||
|
||||
- name: Setup Fastlane
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.4.9'
|
||||
bundler-cache: true
|
||||
|
||||
- name: Build F-Droid with Fastlane
|
||||
env:
|
||||
VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
|
||||
VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
|
||||
run: bundle exec fastlane fdroid_build
|
||||
|
||||
- name: List outputs
|
||||
run: ls -R app/build/outputs/
|
||||
|
||||
- name: Upload F-Droid APK artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: fdroid-apk
|
||||
path: app/build/outputs/apk/fdroid/release/*.apk
|
||||
retention-days: 1
|
||||
|
||||
- name: Attest F-Droid APK provenance
|
||||
if: success()
|
||||
uses: actions/attest-build-provenance@v4
|
||||
with:
|
||||
subject-path: app/build/outputs/apk/fdroid/release/*.apk
|
||||
|
||||
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
|
||||
with:
|
||||
path: ./artifacts
|
||||
|
||||
- name: Create or Update GitHub Release
|
||||
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/**/*
|
||||
draft: true
|
||||
prerelease: true
|
||||
|
||||
- name: Create or Update internal GitHub Release
|
||||
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: false
|
||||
files: ./artifacts/**/*
|
||||
draft: false
|
||||
prerelease: true
|
||||
315
.github/workflows/reusable-check.yml
vendored
315
.github/workflows/reusable-check.yml
vendored
|
|
@ -1,315 +0,0 @@
|
|||
name: Reusable Android Check
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
run_lint:
|
||||
type: boolean
|
||||
default: true
|
||||
run_unit_tests:
|
||||
type: boolean
|
||||
default: true
|
||||
run_coverage:
|
||||
type: boolean
|
||||
default: true
|
||||
run_desktop_builds:
|
||||
type: boolean
|
||||
default: true
|
||||
upload_artifacts:
|
||||
type: boolean
|
||||
default: true
|
||||
secrets:
|
||||
GRADLE_ENCRYPTION_KEY:
|
||||
required: false
|
||||
CODECOV_TOKEN:
|
||||
required: false
|
||||
DATADOG_APPLICATION_ID:
|
||||
required: false
|
||||
DATADOG_CLIENT_TOKEN:
|
||||
required: false
|
||||
GOOGLE_MAPS_API_KEY:
|
||||
required: false
|
||||
GRADLE_CACHE_URL:
|
||||
required: false
|
||||
GRADLE_CACHE_USERNAME:
|
||||
required: false
|
||||
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:
|
||||
# ── 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
|
||||
filter: 'blob:none'
|
||||
submodules: true
|
||||
|
||||
- name: Determine cache read-only setting
|
||||
id: cache_config
|
||||
shell: bash
|
||||
run: |
|
||||
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
|
||||
|
||||
- name: Calculate version code from git commit count
|
||||
id: version_code
|
||||
shell: bash
|
||||
run: |
|
||||
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: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
with:
|
||||
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
cache_read_only: ${{ steps.cache_config.outputs.cache_read_only }}
|
||||
|
||||
- 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: 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:
|
||||
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@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"
|
||||
|
||||
- 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: 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()
|
||||
run: |
|
||||
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
|
||||
|
||||
# ── 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: desktop-app-${{ runner.os }}-${{ runner.arch }}
|
||||
path: desktop/build/compose/binaries/main/app/
|
||||
retention-days: 7
|
||||
145
.github/workflows/scheduled-updates.yml
vendored
145
.github/workflows/scheduled-updates.yml
vendored
|
|
@ -1,145 +0,0 @@
|
|||
name: Scheduled Updates (Firmware, Hardware, Translations)
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- 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-24.04
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
permissions:
|
||||
contents: write # To commit files and push branches
|
||||
pull-requests: write # To create pull requests
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ secrets.CROWDIN_GITHUB_TOKEN }}
|
||||
|
||||
- name: Update firmware releases list
|
||||
run: |
|
||||
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. Skipping firmware update."
|
||||
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
|
||||
|
||||
- name: Update hardware list
|
||||
run: |
|
||||
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. Skipping hardware update."
|
||||
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
|
||||
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
|
||||
commit_message: 'chore(l10n): New Crowdin Translations from scheduled update'
|
||||
push_translations: false
|
||||
push_sources: false
|
||||
localization_branch_name: ${{ github.ref_name }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.CROWDIN_GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
- name: Fix file permissions
|
||||
run: sudo chown -R $USER:$USER .
|
||||
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
with:
|
||||
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
cache_read_only: 'false'
|
||||
|
||||
- name: Update Graphs
|
||||
run: ./gradlew graphUpdate
|
||||
continue-on-error: true
|
||||
|
||||
- name: Create Pull Request if changes occurred
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
token: ${{ secrets.CROWDIN_GITHUB_TOKEN }}
|
||||
commit-message: |
|
||||
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs)
|
||||
|
||||
Automated updates for:
|
||||
- Firmware releases list
|
||||
- Device hardware list
|
||||
- Crowdin source string uploads
|
||||
- Crowdin translation downloads
|
||||
- Module dependency graphs
|
||||
title: 'chore: Scheduled updates (Firmware, Hardware, Translations, Graphs)'
|
||||
body: |
|
||||
This PR includes automated updates from the scheduled workflow:
|
||||
|
||||
- Updated `firmware_releases.json` from the Meshtastic API (if changed).
|
||||
- Updated `device_hardware.json` from the Meshtastic API (if changed).
|
||||
- Source strings were uploaded to Crowdin.
|
||||
- Latest translations were downloaded from Crowdin (if available).
|
||||
- Updated module dependency graphs in README.md files (if changed).
|
||||
|
||||
Please review the changes.
|
||||
branch: 'scheduled-updates'
|
||||
base: 'main'
|
||||
delete-branch: true
|
||||
add-paths: |
|
||||
app/src/main/assets/firmware_releases.json
|
||||
app/src/main/assets/device_hardware.json
|
||||
fastlane/metadata/android/**
|
||||
**/strings.xml
|
||||
**/README.md
|
||||
labels: |
|
||||
automation
|
||||
l10n
|
||||
firmware
|
||||
hardware
|
||||
|
||||
check-workflow-status:
|
||||
name: Check Workflow Status
|
||||
runs-on: ubuntu-24.04-arm
|
||||
permissions: {}
|
||||
needs:
|
||||
- update_assets
|
||||
if: always()
|
||||
steps:
|
||||
- name: Check Workflow Status
|
||||
if: "contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')"
|
||||
run: |
|
||||
echo "One of the dependent jobs failed or was cancelled. Failing the workflow."
|
||||
exit 1
|
||||
26
.github/workflows/stale.yml
vendored
26
.github/workflows/stale.yml
vendored
|
|
@ -1,26 +0,0 @@
|
|||
name: Close Stale Issues
|
||||
on:
|
||||
schedule:
|
||||
- cron: 0 6 * * *
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
stale_issues:
|
||||
name: Close Stale Issues
|
||||
runs-on: ubuntu-24.04-arm
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
|
||||
steps:
|
||||
- name: Stale PR+Issues
|
||||
uses: actions/stale@v10.2.0
|
||||
with:
|
||||
days-before-stale: 30
|
||||
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
|
||||
59
.gitignore
vendored
59
.gitignore
vendored
|
|
@ -1,58 +1,15 @@
|
|||
# Android Studio
|
||||
/.idea/*
|
||||
!/.idea/codeStyles
|
||||
!/.idea/dictionaries/
|
||||
!/.idea/runConfigurations/
|
||||
!/.idea/icon.svg
|
||||
*.iws
|
||||
*.iml
|
||||
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
**/build/**
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
/app/release
|
||||
/buildSrc/build
|
||||
**/debug/**
|
||||
**/release/**
|
||||
|
||||
# Java KeyStore certificates
|
||||
*.jks
|
||||
keystore.properties
|
||||
|
||||
# AGP profiling traces
|
||||
*.trace
|
||||
|
||||
# Kotlin compiler
|
||||
.kotlin
|
||||
|
||||
# VS code
|
||||
.vscode/settings.json
|
||||
|
||||
# Secrets
|
||||
/secrets.properties
|
||||
/fastlane/play-store-credentials.json
|
||||
**/google-services.json
|
||||
|
||||
# Generated library definitions
|
||||
**/src/main/resources/aboutlibraries.json
|
||||
|
||||
/fastlane/report.xml
|
||||
|
||||
/build-logic/convention/build/*
|
||||
/build-logic/build/
|
||||
|
||||
# Personal build scripts
|
||||
build-and-install-android.sh
|
||||
wireless-install.sh
|
||||
|
||||
# Git worktrees
|
||||
.worktrees/
|
||||
/firebase-debug.log.jdk/
|
||||
firebase-debug.log
|
||||
.agent_plans/
|
||||
.agent_refs/
|
||||
.agent_artifacts/
|
||||
/app/release
|
||||
9
.gitmodules
vendored
9
.gitmodules
vendored
|
|
@ -1,3 +1,6 @@
|
|||
[submodule "app proto submodule"]
|
||||
path = core/proto/src/main/proto
|
||||
url = https://github.com/meshtastic/protobufs.git
|
||||
[submodule "app/src/main/proto"]
|
||||
path = app/src/main/proto
|
||||
url = https://github.com/meshtastic/Meshtastic-protobufs.git
|
||||
[submodule "geeksville-androidlib"]
|
||||
path = geeksville-androidlib
|
||||
url = https://github.com/meshtastic/geeksville-androidlib.git
|
||||
|
|
|
|||
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
|
|
@ -0,0 +1 @@
|
|||
Mesh Util
|
||||
40
.idea/codeStyles/Project.xml
generated
40
.idea/codeStyles/Project.xml
generated
|
|
@ -1,48 +1,8 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JavaCodeStyleSettings>
|
||||
<option name="IMPORT_LAYOUT_TABLE">
|
||||
<value>
|
||||
<package name="" withSubpackages="true" static="false" module="true" />
|
||||
<package name="android" withSubpackages="true" static="true" />
|
||||
<package name="androidx" withSubpackages="true" static="true" />
|
||||
<package name="com" withSubpackages="true" static="true" />
|
||||
<package name="junit" withSubpackages="true" static="true" />
|
||||
<package name="net" withSubpackages="true" static="true" />
|
||||
<package name="org" withSubpackages="true" static="true" />
|
||||
<package name="java" withSubpackages="true" static="true" />
|
||||
<package name="javax" withSubpackages="true" static="true" />
|
||||
<package name="" withSubpackages="true" static="true" />
|
||||
<emptyLine />
|
||||
<package name="android" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="androidx" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="com" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="junit" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="net" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="org" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="java" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="javax" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
</value>
|
||||
</option>
|
||||
</JavaCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="PROTO">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
|
|
|
|||
10
.idea/compiler.xml
generated
Normal file
10
.idea/compiler.xml
generated
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel>
|
||||
<module name="app" target="1.8" />
|
||||
<module name="geeksville-androidlib" target="1.7" />
|
||||
<module name="Mesh Util-geeksville-androidlib" target="1.7" />
|
||||
</bytecodeTargetLevel>
|
||||
</component>
|
||||
</project>
|
||||
1
.idea/dictionaries/kevinh.xml
generated
1
.idea/dictionaries/kevinh.xml
generated
|
|
@ -5,7 +5,6 @@
|
|||
<w>errormsg</w>
|
||||
<w>geeksville</w>
|
||||
<w>meshtastic</w>
|
||||
<w>protobuf</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
6
.idea/encodings.xml
generated
Normal file
6
.idea/encodings.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="PROJECT" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
22
.idea/gradle.xml
generated
Normal file
22
.idea/gradle.xml
generated
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="delegatedBuild" value="false" />
|
||||
<option name="testRunner" value="PLATFORM" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/geeksville-androidlib" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
16
.idea/icon.svg
generated
16
.idea/icon.svg
generated
|
|
@ -1,16 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="512" height="512" viewBox="0 0 512 512" xml:space="preserve">
|
||||
<desc>Created with Fabric.js 4.6.0</desc>
|
||||
<defs>
|
||||
</defs>
|
||||
<g transform="matrix(1 0 0 1 256 256)" id="xYQ9Gk9Jwpgj_HMOXB3F_" >
|
||||
<path style="stroke: rgb(213,130,139); stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(103,234,148); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-256, -256)" d="M 0 0 L 512 0 L 512 512 L 0 512 z" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(1.79 0 0 1.79 313.74 258.36)" id="1xBsk2n9FZp60Rz1O-ceJ" >
|
||||
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: round; stroke-miterlimit: 2; fill: rgb(44,45,60); fill-rule: evenodd; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-250.97, -362.41)" d="M 250.908 330.267 L 193.126 415.005 L 180.938 406.694 L 244.802 313.037 C 246.174 311.024 248.453 309.819 250.889 309.816 C 253.326 309.814 255.606 311.015 256.982 313.026 L 320.994 406.536 L 308.821 414.869 L 250.908 330.267 Z" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(1.81 0 0 1.81 145 256.15)" id="KxN7E9YpbyPgz0S4z4Cl6" >
|
||||
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: round; stroke-miterlimit: 2; fill: rgb(44,45,60); fill-rule: evenodd; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-115.14, -528.06)" d="M 87.642 581.398 L 154.757 482.977 L 142.638 474.713 L 75.523 573.134 L 87.642 581.398 Z" stroke-linecap="round" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
17
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
17
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
25
.idea/jarRepositories.xml
generated
Normal file
25
.idea/jarRepositories.xml
generated
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="BintrayJCenter" />
|
||||
<option name="name" value="BintrayJCenter" />
|
||||
<option name="url" value="https://jcenter.bintray.com/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="Google" />
|
||||
<option name="name" value="Google" />
|
||||
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/misc.xml
generated
Normal file
9
.idea/misc.xml
generated
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/runConfigurations.xml
generated
Normal file
12
.idea/runConfigurations.xml
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
13
.idea/saveactions_settings.xml
generated
Normal file
13
.idea/saveactions_settings.xml
generated
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SaveActionSettings">
|
||||
<option name="actions">
|
||||
<set>
|
||||
<option value="activate" />
|
||||
<option value="organizeImports" />
|
||||
<option value="reformat" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="configurationPath" value="" />
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/vcs.xml
generated
Normal file
8
.idea/vcs.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/app/src/main/proto" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/geeksville-androidlib" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
1
.jdk
1
.jdk
|
|
@ -1 +0,0 @@
|
|||
/home/james/.jdks/ms-17.0.18
|
||||
295
.pr5167.diff
295
.pr5167.diff
|
|
@ -1,295 +0,0 @@
|
|||
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 +0,0 @@
|
|||
3.4.9
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Pre-Commit [spotlessApply detekt]" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="--no-parallel" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="spotlessApply" />
|
||||
<option value="detekt" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
|
||||
<EXTENSION ID="com.android.tools.idea.testartifacts.testsuite.GradleRunConfigurationExtension">
|
||||
<com.android.tools.idea.testartifacts.testsuite.SHOW_TEST_RESULT_IN_ANDROID_TEST_SUITE_VIEW>false</com.android.tools.idea.testartifacts.testsuite.SHOW_TEST_RESULT_IN_ANDROID_TEST_SUITE_VIEW>
|
||||
<android.execution.deploysToLocalDevice>false</android.execution.deploysToLocalDevice>
|
||||
</EXTENSION>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<GradleProfilingDisabled>false</GradleProfilingDisabled>
|
||||
<GradleCoverageDisabled>false</GradleCoverageDisabled>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
# 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`
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
# 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
|
||||
```
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
# 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/`
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
# 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`
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
# 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>`.
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
# 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`.
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
# 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.
|
||||
|
||||
108
AGENTS.md
108
AGENTS.md
|
|
@ -1,108 +0,0 @@
|
|||
# Meshtastic Android - Unified Agent & Developer Guide
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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.
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
<copilot_cli_workflow>
|
||||
These tips apply when the agent is the GitHub Copilot CLI. Other agent runtimes may ignore this
|
||||
section.
|
||||
|
||||
- **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>
|
||||
|
||||
<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>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
The Meshtastic Firmware project is subject to the code of conduct for the parent project, which can be found here:
|
||||
https://meshtastic.org/docs/legal/conduct/
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
# Contributing to Meshtastic-Android
|
||||
|
||||
Thank you for your interest in contributing to Meshtastic-Android! We welcome contributions from everyone. Please take a moment to review these guidelines to help us maintain a high-quality, collaborative project.
|
||||
|
||||
## How to Contribute
|
||||
|
||||
- **Fork the repository** and create your branch from `main` or the appropriate feature branch.
|
||||
- **Make your changes** in a logical, atomic manner.
|
||||
- **Test your changes** thoroughly before submitting a pull request.
|
||||
- **Submit a pull request** (PR) with a clear description of your changes and the problem they solve.
|
||||
- If you are addressing an existing issue, please reference it in your PR (e.g., `Fixes #123`).
|
||||
|
||||
## Code Style
|
||||
|
||||
- Follow the [Kotlin Coding Conventions](https://kotlinlang.org/docs/coding-conventions.html) for Kotlin code.
|
||||
- Use Android Studio's default formatting settings.
|
||||
- We use [spotless](https://github.com/diffplug/spotless) for automated code formatting. You can run `./gradlew spotlessApply` to format your code automatically.
|
||||
- You can also run `./gradlew spotlessInstallGitPrePushHook --no-configuration-cache` to install a pre-push Git hook that will run a `spotlessCheck`.
|
||||
- Write clear, descriptive variable and function names.
|
||||
- Add comments where necessary, especially for complex logic.
|
||||
- Keep methods and classes focused and concise.
|
||||
- **Strings:** Use localised strings via the **Compose Multiplatform Resource** library in `:core:resources`.
|
||||
- Do **not** use the legacy `app/src/main/res/values/strings.xml`.
|
||||
- **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
|
||||
|
||||
Text(text = stringResource(Res.string.your_string_key))
|
||||
```
|
||||
|
||||
### Linting
|
||||
|
||||
Meshtastic-Android uses [Detekt](https://detekt.dev/) for static code analysis and linting of Kotlin code.
|
||||
|
||||
- Run `./gradlew detekt` before submitting your pull request to ensure your code passes all lint checks.
|
||||
- Fix any Detekt warnings or errors reported in your code.
|
||||
- It is possible to suppress warnings individually, but this should be used very sparingly.
|
||||
- You can find Detekt configuration in the `config/detekt` directory. If you believe a rule should be changed or suppressed, discuss it in your PR.
|
||||
|
||||
Consistent linting helps keep the codebase clean and maintainable.
|
||||
|
||||
### Testing
|
||||
|
||||
Meshtastic-Android uses unit tests, Robolectric JVM tests, and instrumented UI tests to ensure code quality and reliability.
|
||||
|
||||
- **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 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
|
||||
|
||||
- Add or update tests for any new features or bug fixes.
|
||||
- Ensure all tests pass by running:
|
||||
- `./gradlew test` for unit and Robolectric tests
|
||||
- `./gradlew connectedAndroidTest` for instrumented tests
|
||||
- For UI components, write Robolectric Compose tests where possible for faster execution.
|
||||
- If your change is difficult to test, explain why in your pull request.
|
||||
|
||||
Comprehensive testing helps prevent regressions and ensures a stable experience for all users.
|
||||
|
||||
|
||||
## Pull Requests
|
||||
|
||||
- branches should start with:
|
||||
- bugfix
|
||||
- enhancement
|
||||
- dependencies
|
||||
- repo
|
||||
- reserved (release, automation)
|
||||
- Ensure your branch is up to date with the latest `main` branch before submitting a PR.
|
||||
- Provide a meaningful title and description for your PR.
|
||||
- Include information on how to test and/or replicate if it is not obvious.
|
||||
- Include screenshots or logs if your change affects the UI or user experience.
|
||||
- Be responsive to feedback and make requested changes promptly.
|
||||
- Squash commits if requested by a maintainer.
|
||||
|
||||
## Issue Reporting
|
||||
|
||||
- Search existing issues before opening a new one to avoid duplicates.
|
||||
- Provide a clear and descriptive title.
|
||||
- Include steps to reproduce, expected behavior, and actual behavior.
|
||||
- Attach logs, screenshots, or other helpful context if applicable.
|
||||
|
||||
## Community Standards
|
||||
|
||||
- Be respectful and considerate in all interactions.
|
||||
- The Meshtastic Android project is subject to the code of conduct for the parent project, which can be [found here:](https://meshtastic.org/docs/legal/conduct/)
|
||||
- Help others by reviewing pull requests and answering questions when possible.
|
||||
|
||||
Thank you for helping make Meshtastic-Android better!
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# 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.
|
||||
3
Gemfile
3
Gemfile
|
|
@ -1,3 +0,0 @@
|
|||
source "https://rubygems.org"
|
||||
|
||||
gem "fastlane"
|
||||
238
Gemfile.lock
238
Gemfile.lock
|
|
@ -1,238 +0,0 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.8)
|
||||
abbrev (0.1.2)
|
||||
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.1240.0)
|
||||
aws-sdk-core (3.245.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.123.0)
|
||||
aws-sdk-core (~> 3, >= 3.244.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
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)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
benchmark (0.5.0)
|
||||
bigdecimal (4.1.2)
|
||||
claide (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
csv (3.3.5)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.6.20240107)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.112.0)
|
||||
faraday (1.10.5)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-cookie_jar (0.0.8)
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (>= 1.0.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.1)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.2.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.4)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.4.1)
|
||||
fastlane (2.233.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
abbrev (~> 0.1.2)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.197)
|
||||
babosa (>= 1.0.3, < 2.0.0)
|
||||
base64 (~> 0.2.0)
|
||||
benchmark (>= 0.1.0)
|
||||
bundler (>= 1.17.3, < 5.0.0)
|
||||
colored (~> 1.2)
|
||||
commander (~> 4.6)
|
||||
csv (~> 3.3)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
excon (>= 0.71.0, < 1.0.0)
|
||||
faraday (~> 1.0)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 1.0)
|
||||
fastimage (>= 2.1.0, < 3.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)
|
||||
google-cloud-env (>= 1.6.0, <= 2.1.1)
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
http-cookie (~> 1.0.5)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
logger (>= 1.6, < 2.0)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (>= 2.0.0, < 3.0.0)
|
||||
mutex_m (~> 0.3.0)
|
||||
naturally (~> 2.2)
|
||||
nkf (~> 0.2.0)
|
||||
optparse (>= 0.1.1, < 1.0.0)
|
||||
ostruct (>= 0.1.0)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.5)
|
||||
simctl (~> 1.6.3)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (~> 3)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.4.1)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
fastlane-sirp (1.1.0)
|
||||
gh_inspector (1.1.3)
|
||||
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)
|
||||
googleauth (~> 1.9)
|
||||
httpclient (>= 2.8.3, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
mutex_m
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
google-apis-iamcredentials_v1 (0.26.0)
|
||||
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.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.6.0)
|
||||
google-cloud-storage (1.59.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-core (>= 0.18, < 2)
|
||||
google-apis-iamcredentials_v1 (~> 0.18)
|
||||
google-apis-storage_v1 (>= 0.42)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (~> 1.9)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.11.2)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-env (~> 2.1)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.8)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
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.20.1)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.4.0)
|
||||
naturally (2.3.0)
|
||||
nkf (0.2.0)
|
||||
optparse (0.8.1)
|
||||
os (1.1.4)
|
||||
ostruct (0.6.3)
|
||||
plist (3.7.2)
|
||||
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.4.1)
|
||||
rexml (3.4.4)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
security (0.1.5)
|
||||
signet (0.21.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 4.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
naturally
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
trailblazer-option (0.1.2)
|
||||
tty-cursor (0.7.1)
|
||||
tty-screen (0.8.2)
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (2.6.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.27.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.4.0)
|
||||
rexml (>= 3.3.6, < 4.0)
|
||||
xcpretty (0.4.1)
|
||||
rouge (~> 3.28.0)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-24
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
fastlane
|
||||
|
||||
BUNDLED WITH
|
||||
2.7.2
|
||||
112
README.md
112
README.md
|
|
@ -1,106 +1,34 @@
|
|||
<p align="center">
|
||||
<img src=".github/meshtastic_logo.png" alt="Meshtastic Logo" width="200"/>
|
||||
</p>
|
||||
<h1 align="center">Meshtastic-Android</h1>
|
||||
# Meshtastic-Android
|
||||
|
||||

|
||||
[](https://github.com/meshtastic/Meshtastic-Android/actions/workflows/pull-request.yml)
|
||||
[](https://codecov.io/gh/meshtastic/Meshtastic-Android)
|
||||
[](https://crowdin.meshtastic.org/android)
|
||||
[](https://cla-assistant.io/meshtastic/Meshtastic-Android)
|
||||
[](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 the device side code, see [here](https://github.com/meshtastic/Meshtastic-esp32).
|
||||
|
||||
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 early-alpha, if you have questions or feedback please join our chat [](https://gitter.im/Meshtastic/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge). We would love to hear from you.
|
||||
|
||||
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!
|
||||
Once out of alpha the companion Android application will be released here:
|
||||
|
||||
[](https://play.google.com/store/apps/details?id=com.geeksville.mesh&referrer=utm_source%3Dhomepage%26anid%3Dadmob)
|
||||
|
||||
But if you want the bleeding edge app now, we'd love to have your help testing. Three steps to opt-in to the alpha- test:
|
||||
|
||||
## Get Meshtastic
|
||||
1. Join [this Google group](https://groups.google.com/forum/#!forum/meshtastic-alpha-testers) with the account you use in Google Play.
|
||||
2. Go to this [URL](https://play.google.com/apps/testing/com.geeksville.mesh) to opt-in to the alpha test.
|
||||
3. If you encounter any problems or have questions, post in our gitter chat and we'll help.
|
||||
|
||||
The easiest, and fastest way to get the latest beta releases is to use our [github releases](https://github.com/meshtastic/Meshtastic-Android/releases). It is recommend to use these with [Obtainium](https://github.com/ImranR98/Obtainium) to get the latest updates.
|
||||
## Analytics setup
|
||||
|
||||
Alternatively, these other providers are also available, but may be slower to update.
|
||||
Once this project is public, I'll happily let collaborators have access to the crash logs/analytics.
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
width="24%">](https://f-droid.org/packages/com.geeksville.mesh/)
|
||||
[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png"
|
||||
alt="Get it on IzzyOnDroid"
|
||||
width="24%">](https://apt.izzysoft.de/fdroid/index/apk/com.geeksville.mesh)
|
||||
[<img src="https://github.com/machiav3lli/oandbackupx/blob/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png"
|
||||
alt="Get it on GitHub"
|
||||
width="24%">](https://github.com/meshtastic/Meshtastic-Android/releases)
|
||||
[<img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"
|
||||
alt="Download at https://play.google.com/store/apps/details?id=com.geeksville.mesh]"
|
||||
width="24%">](https://play.google.com/store/apps/details?id=com.geeksville.mesh&referrer=utm_source%3Dgithub-android-readme)
|
||||
* analytics is currently on, before beta is over I'll make it optional
|
||||
* on dev devices "adb shell setprop debug.firebase.analytics.app com.geeksville.mesh"
|
||||
adb shell setprop log.tag.FirebaseCrashlytics DEBUG
|
||||
* To see analytics: https://console.firebase.google.com/u/0/project/meshutil/analytics/app/android:com.geeksville.mesh/overview
|
||||
* To see crash logs: https://console.firebase.google.com/u/0/project/meshutil/crashlytics/app/android:com.geeksville.mesh/issues?state=open&time=last-seven-days&type=crash
|
||||
|
||||
The play store is the last to update of these options, but if you want to join the Play Store testing program go to [this URL](https://play.google.com/apps/testing/com.geeksville.mesh) and opt-in to become a tester.
|
||||
If you encounter any problems or have questions, [ask us on the discord](https://discord.gg/meshtastic), [create an issue](https://github.com/meshtastic/Meshtastic-Android/issues), or [post in the forum](https://github.com/orgs/meshtastic/discussions) and we'll help as we can.
|
||||
|
||||
## Documentation
|
||||
|
||||
The project's documentation is generated with [Dokka](https://kotlinlang.org/docs/dokka-introduction.html) and hosted on GitHub Pages. It is automatically updated on every push to the `main` branch.
|
||||
|
||||
[**View Documentation**](https://meshtastic.github.io/Meshtastic-Android/)
|
||||
|
||||
### Generating Locally
|
||||
|
||||
You can generate the documentation locally to preview your changes.
|
||||
|
||||
1. **Run the Dokka task:**
|
||||
```bash
|
||||
./gradlew dokkaGeneratePublicationHtml
|
||||
```
|
||||
2. **View the output:**
|
||||
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, 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:** 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 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
|
||||
|
||||
You can help translate the app into your native language using [Crowdin](https://crowdin.meshtastic.org/android).
|
||||
|
||||
## API & Integration
|
||||
|
||||
Developers can integrate with the Meshtastic Android app using our published API library via **JitPack**. This allows third-party applications (like the ATAK plugin) to communicate with the mesh service via AIDL.
|
||||
|
||||
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.
|
||||
|
||||
https://meshtastic.org/docs/development/android/
|
||||
|
||||
Note: when building the `google` flavor locally you will need to supply your own [Google Maps Android SDK api key](https://developers.google.com/maps/documentation/android-sdk/get-api-key) `MAPS_API_KEY` in `local.properties` in order to use Google Maps.
|
||||
e.g.
|
||||
```properties
|
||||
MAPS_API_KEY=your_google_maps_api_key_here
|
||||
for verbose logging:
|
||||
```aidl
|
||||
adb shell setprop log.tag.FA VERBOSE
|
||||
```
|
||||
|
||||
## Contributing guidelines
|
||||
Copyright 2018, S. Kevin Hester-Chow, kevinh@geeksville.com. GPL V3 license
|
||||
|
||||
For detailed instructions on how to contribute, please see our [CONTRIBUTING.md](CONTRIBUTING.md) file.
|
||||
For details on our release process, see the [RELEASE_PROCESS.md](RELEASE_PROCESS.md) file.
|
||||
|
||||
## Repository Statistics
|
||||
|
||||

|
||||
|
||||
Copyright 2025, Meshtastic LLC. GPL-3.0 license
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
# Meshtastic-Android Release Process
|
||||
|
||||
This guide summarizes the steps for releasing a new version of Meshtastic-Android. The process is fully automated via GitHub Actions and Fastlane.
|
||||
|
||||
## Overview
|
||||
|
||||
The entire release process is managed by a single, manually-triggered GitHub Action: **`Create or Promote Release`**.
|
||||
|
||||
- **Trigger:** To start a new release or promote an existing one, a developer manually runs the workflow from the GitHub Actions tab.
|
||||
- **Inputs:** The workflow requires two inputs:
|
||||
1. `version`: The base version number you are releasing (e.g., `2.4.0`).
|
||||
2. `channel`: The release channel you are targeting (`internal`, `closed`, `open`, or `production`).
|
||||
- **Automation:** The workflow handles everything automatically:
|
||||
- **Syncs Assets:** Fetches the latest firmware/hardware lists, protobuf definitions, and translations (Crowdin).
|
||||
- **Generates Changelog:** Creates a clean changelog from commits since the last production release and commits it to the repo.
|
||||
- **Updates Config:** Automatically bumps the `VERSION_NAME_BASE` in `config.properties`.
|
||||
- **Verifies & Tags:** Runs lint checks, builds the app, and *only* tags the release if successful.
|
||||
- **Deploys:** Uploads the build to the correct Google Play track and attaches artifacts (`.aab`/`.apk`) to a GitHub Release.
|
||||
- **Changelog:** Release notes are auto-generated from PR labels. Ensure PRs are labeled correctly to maintain an accurate changelog.
|
||||
|
||||
## Release Steps
|
||||
|
||||
### 1. Start an Internal Release
|
||||
|
||||
1. Navigate to the **Actions** tab in the GitHub repository.
|
||||
2. Select the **`Create or Promote Release`** workflow.
|
||||
3. Click the **"Run workflow"** dropdown.
|
||||
4. Enter the base `version` (e.g., `2.4.0`).
|
||||
5. Select the `internal` channel.
|
||||
6. Click **"Run workflow"**.
|
||||
|
||||
The workflow will:
|
||||
1. **Create a new commit** on the current branch containing updated assets, translations, and the new changelog.
|
||||
2. **Tag** that commit with an incremental internal tag (e.g., `v2.4.0-internal.1`).
|
||||
3. **Build & Deploy** the verified artifact to the Play Store Internal track.
|
||||
4. Publish a **draft** pre-release on GitHub.
|
||||
|
||||
### 2. Promote to the Next Channel
|
||||
|
||||
Once an internal build has been verified, you can promote it to a wider audience.
|
||||
|
||||
1. Run the **`Create or Promote Release`** workflow again with the same base `version`.
|
||||
2. Select the next channel in the sequence (e.g., `closed`, then `open`).
|
||||
3. The workflow will create a new incremental tag for that channel (e.g., `v2.4.0-closed.1`) and create a **published** pre-release on GitHub.
|
||||
|
||||
### 3. Promote to Production
|
||||
|
||||
After testing is complete on all pre-release channels, you can create the final public release.
|
||||
|
||||
1. Run the **`Create or Promote Release`** workflow one last time.
|
||||
2. Use the same base `version`.
|
||||
3. Select the `production` channel.
|
||||
4. The workflow will create a clean version tag (e.g., `v2.4.0`) and create a **published, stable** (non-prerelease) release on GitHub.
|
||||
|
||||
### 4. Post-Release
|
||||
|
||||
1. **Verify:** Check the Google Play Console to ensure the build is available on the correct track.
|
||||
2. **Merge:** Merge the release branch (if one was used for stabilization) back into `main`.
|
||||
|
||||
## Build Attestations & Provenance
|
||||
|
||||
All release artifacts are accompanied by explicit GitHub build attestations (provenance). This provides cryptographic proof that the artifacts were built by our trusted GitHub Actions workflow, ensuring supply chain integrity.
|
||||
|
||||
- You can view and verify provenance in the GitHub UI under each release asset.
|
||||
- For more details, see [GitHub's documentation on build provenance](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#provenance-attestations).
|
||||
12
SECURITY.md
12
SECURITY.md
|
|
@ -1,12 +0,0 @@
|
|||
# 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.
|
||||
163
TODO.md
Normal file
163
TODO.md
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# High priority
|
||||
Work items for soon alpha builds
|
||||
|
||||
* update play store listing for public beta
|
||||
* run services in sim mode on emulator
|
||||
* show offline nodes as greyed out
|
||||
* show time since last contact on the node info card
|
||||
* show pointer arrow on the outside of the user icons, always pointing towoards them
|
||||
* fix app icon in title bar
|
||||
* show direction on the nodeinfo cards
|
||||
* take video of the app
|
||||
* register app link for our URLs https://developer.android.com/studio/write/app-link-indexing.html
|
||||
* let users change & share channels (but no saving them yet)
|
||||
* treat macaddrs as the unique id, not the app layer user id
|
||||
* only publish gps positions once every 5 mins while we are connected to our radio _and_ someone else is in the mesh
|
||||
|
||||
# Medium priority
|
||||
Features for future builds
|
||||
|
||||
* fix notification setSmallIcon parameter - change it to use the meshtastic icon
|
||||
* ditch compose and use https://github.com/zsmb13/MaterialDrawerKt + https://github.com/Kotlin/anko/wiki/Anko-Layouts?
|
||||
* describe user experience: devices always point to each other and show distance, you can send texts between nodes
|
||||
the channel is encrypted, you can share the the channel key with others by qr code or by sharing a special link
|
||||
* be smarter about sharing GPS location with the device (to save power), integrate with new network scheduler
|
||||
* stop scan when we start the service - I think this is done, but check
|
||||
* set the radio macaddr by using the service (not by slamming bytes into the Preferences)
|
||||
* show bt scan progress centered and towards the bottom of the screen
|
||||
* when a text arrives, move that node info card to the bottom on the window - put the text to the left of the card. with a small arrow/distance/shortname
|
||||
* let the user type texts somewhere
|
||||
* use this for preferences? https://developer.android.com/guide/topics/ui/settings/
|
||||
* at connect we might receive messages before finished downloading the nodeinfo. In that case, process those messages later
|
||||
* test with oldest compatible android in emulator (see below for testing with hardware)
|
||||
* add play store link with https://developers.google.com/analytics/devguides/collection/android/v4/campaigns#google-play-url-builder and the play icon
|
||||
|
||||
# Signal alpha release
|
||||
Do this "Signal app compatible" release relatively soon after the alpha release of the android app.
|
||||
|
||||
* call onNodeDBChanged after we haven't heard a packet from the mesh in a while - because that's how we decide we have less than 2 nodes in the mesh and should stop listening to the local GPS
|
||||
* add large packet reassembly?
|
||||
* optionally turn off crypto in signal - preferably though see if there is a nice way to be a peer of signal/sms and now mesh.
|
||||
* change signal package ID - if distributing modified binary
|
||||
* investigate the Signal SMS message flow path, see if I could just make Mesh a third peer to signal & sms?
|
||||
* make signal work when there is no internet up
|
||||
* make Signal rx path work
|
||||
* send Signal message type. It seems to be? " public static final int WHISPER_TYPE = 2;
|
||||
public static final int PREKEY_TYPE = 3;
|
||||
public static final int SENDERKEY_TYPE = 4;
|
||||
public static final int SENDERKEY_DISTRIBUTION_TYPE = 5;"
|
||||
|
||||
# Medium priority
|
||||
Things for the betaish period.
|
||||
|
||||
* Use setLargeIcon to show user icons in the notification: file:///home/kevinh/packages/android-sdk-linux/docs/design/patterns/notifications.html
|
||||
* Our notification about messages should use VISIBLITY_PRIVATE + setPublicVersion per file:///home/kevinh/packages/android-sdk-linux/docs/guide/topics/ui/notifiers/notifications.html
|
||||
* let users save old channels
|
||||
* make sw update work while node is connected to mesh - the problem is there are _two_ instances of SafeBluetooth at that moment and therefore we violate undocumented android mutex
|
||||
rules at the BluetoothDevice level. Either make SafeBluetooth lock at the device level or (not as good) make SafeBluetooth a singleton.
|
||||
* Use LocationRequest.setSmallestDisplacement to save battery and decrease net activity
|
||||
* MeshService.reinitFromRadio can take 300 ms, run it in a worker thread instead
|
||||
* show user icons in chat
|
||||
* keep past messages in db, one db per channel
|
||||
* spend some quality power consumption tuning with https://developer.android.com/studio/profile/energy-profiler and https://developer.android.com/topic/performance/power/battery-historian
|
||||
* Do PRIORITY_BALANCED_POWER_ACCURACY for our gps updates when no one in the mesh is nearer than 200 meters
|
||||
* fix slow rendering warnings in play console
|
||||
* use google signin to get user name
|
||||
* use Firebase Test Lab
|
||||
* let user pick/specify a name through ways other than google signin (for the privacy concerned, or devices without Play API)
|
||||
* make my android app show mesh state
|
||||
* show qr code for each channel https://medium.com/@aanandshekharroy/generate-barcode-in-android-app-using-zxing-64c076a5d83a
|
||||
* let user change radio params and share radio join info via QR code or text message (use an encoded app specific URL - to autoprompt for app installation as needed)
|
||||
* test with an oldish android release using real hardware
|
||||
* stop using a foreground service
|
||||
* use platform theme (dark or light)
|
||||
* remove mixpanel analytics
|
||||
* require user auth to pair with the device (i.e. press button on device to allow a new phone to pair with it).
|
||||
Don't leave device discoverable. Don't let unpaired users do things with device
|
||||
* if the rxpacket queue on the device overflows (because android hasn't connected in a while) send a special packet to android which means 'X packets have been dropped because you were offline' -drop oldest packets first
|
||||
|
||||
# Low priority
|
||||
|
||||
** make analytics optional
|
||||
* also add a receiver that fires after a new update was installed from the play store
|
||||
|
||||
# Done
|
||||
|
||||
* DONE fix bluetooth update
|
||||
* DONE refactor sw update code to share with my other bluetooth service
|
||||
* DONE don't let sw update got to sleep during the update
|
||||
* assert() is apparently a noop - change to use my version of assert
|
||||
* DONE add crash reporting
|
||||
* DONE add analytics (make them optional)
|
||||
* make frontend using https://developer.android.com/jetpack/compose/tutorial
|
||||
* change bluetooth mtu length to 512 (default is only 20)
|
||||
* DONE get signal running under debugger
|
||||
* Find good Signal hooks
|
||||
* receive fake packets at power on to built initial state (for debugging, pretend there are a couple of nodes out there)
|
||||
* learn our node number
|
||||
* test mesh service from activity
|
||||
* DONE handle failures in onCharWrite, instead of logAssert - because they can happen if device goes away
|
||||
* DONE explictly broadcast towards signal https://developer.android.com/guide/components/broadcasts
|
||||
* make test implementation of android service (doesn't use bluetooth)
|
||||
* undo base64
|
||||
* use android service from Signal
|
||||
* send signal message type over wire
|
||||
* DONE add broadcasters for use by signal (node changes and packet received)
|
||||
* DONE have signal declare receivers: https://developer.android.com/guide/components/broadcasts#manifest-declared-receivers
|
||||
* fix // FIXME hack for now - throw IdNotFoundException(id) in MeshService
|
||||
* clean up sw update code in device side
|
||||
* add real messaging code/protobufs
|
||||
* implement android side of mesh radio bluetooth link
|
||||
* use the lora net code on my current protoboard
|
||||
* investigate a 16 bit node number. If possible it would make collisions super rare. Much easier to just pick a nodenum and go.
|
||||
* remove example code boilerplate from the service
|
||||
* switch from protobuf-java to protobuf-javalite - much faster and smaller, just no JSON debug printing
|
||||
* have phone use our local node number as its node number (instead of hardwired)
|
||||
* if radio disconnects, we need to requeue a new connect attempt in RadioService
|
||||
* don't do mesh based algoritm for node id assignment (initially) - instead just store in flash - possibly even in the initial alpha release do this hack
|
||||
* show connection state on gui
|
||||
* parcels are busted - something wrong with the Parcelize kotlin magic
|
||||
* add app icon
|
||||
* when notified phone should automatically download messages
|
||||
* use https://codelabs.developers.google.com/codelabs/jetpack-compose-basics/#4 to show service state
|
||||
* all chat in the app defaults to group chat
|
||||
* start bt receive on boot
|
||||
* warn user to bt pair
|
||||
* suppress logging output if running a release build (required for play store)
|
||||
* provide gps location for devices that don't have it
|
||||
* prompt user to turnon bluetooth and bind
|
||||
* show real ID of me when I sent texts
|
||||
* keep text entry box at bottom of screen
|
||||
* when I enter texts, send them to the device
|
||||
* let user set name and shortname
|
||||
* let user send texts
|
||||
* get rid of green bar at top
|
||||
* change titlebar based off which screen we are showing
|
||||
* on onStop somehow stop the BT scan (to prevent burning battery)
|
||||
* "new bluetooth connection state 0, status 22" loss of connection
|
||||
* fix BT device scanning - make a setup screen
|
||||
* tell Compose geeks
|
||||
* call crashlytics from exceptionReporter!!! currently not logging failures caught there
|
||||
* do setOwner every time we connect to the radio, use our settings, radio should ignore if unchanged
|
||||
* send location data for devices that don't have a GPS - https://developer.android.com/training/location/change-location-settings
|
||||
* if necessary restart entire BT adapter with this tip from Michael https://stackoverflow.com/questions/35103701/ble-android-onconnectionstatechange-not-being-called
|
||||
* don't show test texts when not under emulator
|
||||
* make node list view not look like ass
|
||||
* test bt boot behavior
|
||||
* include tent on cloud graphics, so redraws work properly
|
||||
* record analytics events when radio connects/disconnects, include # of nodes in mesh
|
||||
* make a boot screen explaining this is an early alpha, tell user to go to settings if they have a radio, otherwise go to website
|
||||
* when we connect to radio, distances to nodes in the chat log should automatically redraw
|
||||
* add screenshots and text to play store entry
|
||||
* if no radio is selected, launch app on the radio select screen
|
||||
* connect to bluetooth device automatically using minimum power - start looking at phone boot
|
||||
* tell various vendors & post in forums
|
||||
* change info() log strings to debug()
|
||||
* have the foreground service's notification show a summary of network status "connected/disconnected, 5 of 6 nodes, nearest: kevin 5km",
|
||||
* have notification (individually maskable) notifications for received texts - use file:///home/kevinh/packages/android-sdk-linux/docs/reference/android/support/v4/app/NotificationCompat.BigTextStyle.html
|
||||
* startforegroundservice only if we have a valid radio
|
||||
* when we select a new radio, restart the service
|
||||
* make channel button look like a button
|
||||
* generate real channel QR codes
|
||||
* Have play store entry ask users to report if their android version is too old to allow install
|
||||
* use git submodule for androidlib
|
||||
4
app/.gitignore
vendored
Normal file
4
app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/build
|
||||
/debug
|
||||
/release
|
||||
google-services.json
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
# `:app`
|
||||
|
||||
## Overview
|
||||
The `:app` module is the entry point for the Meshtastic Android application. It orchestrates the various feature modules, manages global state, and provides the main UI shell.
|
||||
|
||||
## Key Components
|
||||
|
||||
### 1. `MainActivity` & `Main.kt`
|
||||
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. 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. Koin Application
|
||||
`MeshUtilApplication` is the Koin entry point, providing the global dependency injection container.
|
||||
|
||||
## Architecture
|
||||
The module primarily serves as a "glue" layer, connecting:
|
||||
- `core:*` modules for shared logic.
|
||||
- `feature:*` modules for specific user-facing screens.
|
||||
|
||||
## Module dependency graph
|
||||
|
||||
<!--region graph-->
|
||||
```mermaid
|
||||
graph TB
|
||||
:app[app]:::android-application
|
||||
: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
|
||||
:app -.-> :core:nfc
|
||||
:app -.-> :core:prefs
|
||||
:app -.-> :core:proto
|
||||
:app -.-> :core:service
|
||||
: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;
|
||||
|
||||
```
|
||||
<!--endregion-->
|
||||
122
app/build.gradle
Normal file
122
app/build.gradle
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
||||
// Apply the Crashlytics Gradle plugin
|
||||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
|
||||
// protobuf
|
||||
apply plugin: 'com.google.protobuf'
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion "29.0.2"
|
||||
defaultConfig {
|
||||
applicationId "com.geeksville.mesh"
|
||||
minSdkVersion 22 // The oldest emulator image I have tried is 22 (though 21 probably works)
|
||||
targetSdkVersion 29
|
||||
versionCode 103
|
||||
versionName "0.1.3"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
// Enables Jetpack Compose for this module
|
||||
compose true // NOTE, if true main app crashes if you use regular view layout functions
|
||||
}
|
||||
|
||||
// Set both the Java and Kotlin compilers to target Java 8.
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerVersion "1.3.61-dev-withExperimentalGoogleExtensions-20200129"
|
||||
kotlinCompilerExtensionVersion "0.1.0-dev06"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
// per protobuf-gradle-plugin docs, this is recommended for android
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.9.0'
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
task.builtins {
|
||||
java {
|
||||
// turned off for now so I can use json printing
|
||||
option "lite"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.core:core-ktx:1.2.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
|
||||
// You need to depend on the lite runtime library, not protobuf-java
|
||||
// For now I'm not using javalite, because I want JSON printing
|
||||
//implementation 'com.google.protobuf:protobuf-java:3.11.1'
|
||||
//implementation 'com.google.protobuf:protobuf-java-util:3.11.1'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.11.1'
|
||||
|
||||
// You also need to include the following Compose toolkit dependencies.
|
||||
implementation("androidx.compose:compose-runtime:$compose_version")
|
||||
implementation("androidx.ui:ui-graphics:$compose_version")
|
||||
implementation("androidx.ui:ui-layout:$compose_version")
|
||||
implementation("androidx.ui:ui-material:$compose_version")
|
||||
implementation("androidx.ui:ui-unit:$compose_version")
|
||||
implementation("androidx.ui:ui-util:$compose_version")
|
||||
implementation "androidx.ui:ui-tooling:$compose_version"
|
||||
androidTestImplementation("androidx.ui:ui-platform:$compose_version")
|
||||
androidTestImplementation("androidx.ui:ui-test:$compose_version")
|
||||
|
||||
// location services
|
||||
implementation 'com.google.android.gms:play-services-location:17.0.0'
|
||||
|
||||
// For Google Sign-In (owner name accesss)
|
||||
implementation 'com.google.android.gms:play-services-auth:17.0.0'
|
||||
|
||||
// Add the Firebase SDK for Crashlytics.
|
||||
implementation 'com.google.firebase:firebase-crashlytics:17.0.0-beta01'
|
||||
|
||||
// alas implementation bug deep in the bowels when I tried it for my SyncBluetoothDevice class
|
||||
// implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3"
|
||||
|
||||
// add SDKs for any other desired Firebase products
|
||||
// https://firebase.google.com/docs/android/setup#available-libraries
|
||||
|
||||
// barcode support
|
||||
// implementation('com.google.zxing:core:3.4.0')
|
||||
implementation('com.journeyapps:zxing-android-embedded:3.6.0')
|
||||
|
||||
implementation project(':geeksville-androidlib')
|
||||
}
|
||||
|
|
@ -1,338 +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/>.
|
||||
*/
|
||||
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import com.mikepenz.aboutlibraries.plugin.DuplicateMode
|
||||
import com.mikepenz.aboutlibraries.plugin.DuplicateRule
|
||||
import org.meshtastic.buildlogic.GitVersionValueSource
|
||||
import org.meshtastic.buildlogic.configProperties
|
||||
import java.io.FileInputStream
|
||||
import java.util.Properties
|
||||
|
||||
val gitVersionProvider = providers.of(GitVersionValueSource::class.java) {}
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.meshtastic.android.application)
|
||||
alias(libs.plugins.meshtastic.android.application.flavors)
|
||||
alias(libs.plugins.meshtastic.android.application.compose)
|
||||
id("meshtastic.koin")
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
alias(libs.plugins.secrets)
|
||||
alias(libs.plugins.aboutlibraries)
|
||||
id("dev.mokkery")
|
||||
}
|
||||
|
||||
val keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
val keystoreProperties = Properties()
|
||||
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
FileInputStream(keystorePropertiesFile).use { keystoreProperties.load(it) }
|
||||
}
|
||||
|
||||
configure<ApplicationExtension> {
|
||||
namespace = "org.meshtastic.app"
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String?
|
||||
keyPassword = keystoreProperties["keyPassword"] as String?
|
||||
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
|
||||
storePassword = keystoreProperties["storePassword"] as String?
|
||||
}
|
||||
}
|
||||
defaultConfig {
|
||||
applicationId = configProperties.getProperty("APPLICATION_ID")
|
||||
|
||||
val vcOffset = configProperties.getProperty("VERSION_CODE_OFFSET")?.toInt() ?: 0
|
||||
println("Version code offset: $vcOffset")
|
||||
versionCode =
|
||||
(
|
||||
project.findProperty("android.injected.version.code")?.toString()?.toInt()
|
||||
?: System.getenv("VERSION_CODE")?.toInt()
|
||||
?: (gitVersionProvider.get().toInt() + vcOffset)
|
||||
)
|
||||
versionName =
|
||||
(
|
||||
project.findProperty("android.injected.version.name")?.toString()
|
||||
?: System.getenv("VERSION_NAME")
|
||||
?: configProperties.getProperty("VERSION_NAME_BASE")
|
||||
)
|
||||
buildConfigField("String", "MIN_FW_VERSION", "\"${configProperties.getProperty("MIN_FW_VERSION")}\"")
|
||||
buildConfigField("String", "ABS_MIN_FW_VERSION", "\"${configProperties.getProperty("ABS_MIN_FW_VERSION")}\"")
|
||||
// We have to list all translated languages here,
|
||||
// because some of our libs have bogus languages that google play
|
||||
// doesn't like and we need to strip them (gr)
|
||||
@Suppress("UnstableApiUsage")
|
||||
val ci = project.findProperty("ci")?.toString()?.toBoolean() ?: false
|
||||
if (ci) {
|
||||
println("CI build detected - limiting locale filters for faster packaging")
|
||||
androidResources.localeFilters.addAll(listOf("en"))
|
||||
} else {
|
||||
androidResources.localeFilters.addAll(
|
||||
listOf(
|
||||
"en",
|
||||
"ar",
|
||||
"bg",
|
||||
"ca",
|
||||
"cs",
|
||||
"de",
|
||||
"el",
|
||||
"es",
|
||||
"et",
|
||||
"fi",
|
||||
"fr",
|
||||
"ga",
|
||||
"gl",
|
||||
"hr",
|
||||
"ht",
|
||||
"hu",
|
||||
"is",
|
||||
"it",
|
||||
"iw",
|
||||
"ja",
|
||||
"ko",
|
||||
"lt",
|
||||
"nl",
|
||||
"no",
|
||||
"pl",
|
||||
"pt",
|
||||
"pt-rBR",
|
||||
"ro",
|
||||
"ru",
|
||||
"sk",
|
||||
"sl",
|
||||
"sq",
|
||||
"sr",
|
||||
"srp",
|
||||
"sv",
|
||||
"tr",
|
||||
"uk",
|
||||
"zh-rCN",
|
||||
"zh-rTW",
|
||||
),
|
||||
)
|
||||
}
|
||||
ndk { abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") }
|
||||
|
||||
val disableSplits =
|
||||
project.gradle.startParameter.taskNames.any {
|
||||
it.contains("bundle", ignoreCase = true) || it.contains("google", ignoreCase = true)
|
||||
}
|
||||
|
||||
// Enable ABI splits to generate smaller APKs per architecture for F-Droid/IzzyOnDroid
|
||||
splits {
|
||||
abi {
|
||||
isEnable = !disableSplits
|
||||
reset()
|
||||
include("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
isUniversalApk = true
|
||||
}
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
// Disables dependency metadata when building APKs (for IzzyOnDroid/F-Droid)
|
||||
includeInApk = false
|
||||
// Disables dependency metadata when building Android App Bundles (for Google Play)
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
// Configure existing product flavors (defined by convention plugin)
|
||||
// with their dynamic version names.
|
||||
productFlavors {
|
||||
configureEach {
|
||||
versionName = "${defaultConfig.versionName} (${defaultConfig.versionCode}) $name"
|
||||
if (name == "google") {
|
||||
manifestPlaceholders["MAPS_API_KEY"] = "dummy"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
if (keystoreProperties["storeFile"] != null) {
|
||||
signingConfig = signingConfigs.named("release").get()
|
||||
} else {
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
isDebuggable = false
|
||||
}
|
||||
}
|
||||
bundle { language { enableSplit = false } }
|
||||
|
||||
testOptions { unitTests { isIncludeAndroidResources = true } }
|
||||
}
|
||||
|
||||
secrets {
|
||||
defaultPropertiesFileName = "secrets.defaults.properties"
|
||||
propertiesFileName = "secrets.properties"
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
onVariants(selector().withBuildType("debug")) { variant ->
|
||||
variant.flavorName?.let { flavor -> variant.applicationId = "com.geeksville.mesh.$flavor.debug" }
|
||||
}
|
||||
|
||||
onVariants(selector().withBuildType("release")) { variant ->
|
||||
if (variant.flavorName == "google") {
|
||||
val variantNameCapped = variant.name.replaceFirstChar { it.uppercase() }
|
||||
val minifyTaskName = "minify${variantNameCapped}WithR8"
|
||||
val uploadTaskName = "uploadMapping$variantNameCapped"
|
||||
if (project.tasks.findByName(uploadTaskName) != null && project.tasks.findByName(minifyTaskName) != null) {
|
||||
tasks.named(minifyTaskName).configure { finalizedBy(uploadTaskName) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
project.afterEvaluate {
|
||||
logger.lifecycle(
|
||||
"Version code is set to: ${extensions.getByType<ApplicationExtension>().defaultConfig.versionCode}",
|
||||
)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
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)
|
||||
implementation(projects.core.proto)
|
||||
implementation(projects.core.service)
|
||||
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.jetbrains.compose.material3.adaptive)
|
||||
implementation(libs.jetbrains.compose.material3.adaptive.layout)
|
||||
implementation(libs.jetbrains.compose.material3.adaptive.navigation)
|
||||
implementation(libs.material)
|
||||
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.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.usb.serial.android)
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
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.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)
|
||||
|
||||
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.robolectric)
|
||||
testImplementation(libs.androidx.test.core)
|
||||
testImplementation(libs.compose.multiplatform.ui.test)
|
||||
testImplementation(libs.androidx.test.ext.junit)
|
||||
testImplementation(libs.androidx.glance.appwidget)
|
||||
}
|
||||
|
||||
aboutLibraries {
|
||||
// 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,5 +0,0 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues/>
|
||||
</SmellBaseline>
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
{
|
||||
"project_info": {
|
||||
"project_number": "1111",
|
||||
"firebase_url": "https://xxx.firebaseio.com",
|
||||
"project_id": "xxx",
|
||||
"storage_bucket": "xxx.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:xxx:android:1111",
|
||||
"android_client_info": {
|
||||
"package_name": "com.geeksville.mesh"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "111-xxx.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "com.geeksville.mesh"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "111-xxx.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "com.geeksville.mesh"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "111-xxx.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "APlaceholderAPIKeyWith-ThirtyNineCharsX"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "111-xxx.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:xxx:android:1111",
|
||||
"android_client_info": {
|
||||
"package_name": "com.geeksville.mesh.google.debug"
|
||||
}
|
||||
},
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "APlaceholderAPIKeyWith-ThirtyNineCharsX"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
1
app/google-services.json
Symbolic link
1
app/google-services.json
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../google-services.json
|
||||
58
app/proguard-rules.pro
vendored
58
app/proguard-rules.pro
vendored
|
|
@ -1,45 +1,21 @@
|
|||
# ============================================================================
|
||||
# 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.
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# 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.
|
||||
# ============================================================================
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# ---- General ----------------------------------------------------------------
|
||||
# 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 *;
|
||||
#}
|
||||
|
||||
# Open-source — no need to obfuscate
|
||||
-dontobfuscate
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# ---- Networking (transitive references from Ktor on Android) ----------------
|
||||
|
||||
-dontwarn org.conscrypt.**
|
||||
-dontwarn org.bouncycastle.**
|
||||
-dontwarn org.openjsse.**
|
||||
|
||||
# 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.
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
package com.geeksville.mesh
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.geeksville.com.geeksville.mesh", appContext.packageName)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,216 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 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.cluster;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Point;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import org.meshtastic.app.map.model.MarkerWithLabel;
|
||||
|
||||
import org.osmdroid.util.BoundingBox;
|
||||
import org.osmdroid.views.MapView;
|
||||
import org.osmdroid.views.overlay.Overlay;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.ListIterator;
|
||||
|
||||
/**
|
||||
* An overlay allowing to perform markers clustering.
|
||||
* Usage: put your markers inside with add(Marker), and add the MarkerClusterer to the map overlays.
|
||||
* Depending on the zoom level, markers will be displayed separately, or grouped as a single Marker. <br/>
|
||||
*
|
||||
* This abstract class provides the framework. Sub-classes have to implement the clustering algorithm,
|
||||
* and the rendering of a cluster.
|
||||
*
|
||||
* @author M.Kergall
|
||||
*
|
||||
*/
|
||||
public abstract class MarkerClusterer extends Overlay {
|
||||
|
||||
/** impossible value for zoom level, to force clustering */
|
||||
protected static final int FORCE_CLUSTERING = -1;
|
||||
|
||||
protected ArrayList<MarkerWithLabel> mItems = new ArrayList<MarkerWithLabel>();
|
||||
protected Point mPoint = new Point();
|
||||
protected ArrayList<StaticCluster> mClusters = new ArrayList<StaticCluster>();
|
||||
protected int mLastZoomLevel;
|
||||
protected Bitmap mClusterIcon;
|
||||
protected String mName, mDescription;
|
||||
|
||||
// abstract methods:
|
||||
|
||||
/** clustering algorithm */
|
||||
public abstract ArrayList<StaticCluster> clusterer(MapView mapView);
|
||||
/** Build the marker for a cluster. */
|
||||
public abstract MarkerWithLabel buildClusterMarker(StaticCluster cluster, MapView mapView);
|
||||
/** build clusters markers to be used at next draw */
|
||||
public abstract void renderer(ArrayList<StaticCluster> clusters, Canvas canvas, MapView mapView);
|
||||
|
||||
public MarkerClusterer() {
|
||||
super();
|
||||
mLastZoomLevel = FORCE_CLUSTERING;
|
||||
}
|
||||
|
||||
public void setName(String name){
|
||||
mName = name;
|
||||
}
|
||||
|
||||
public String getName(){
|
||||
return mName;
|
||||
}
|
||||
|
||||
public void setDescription(String description){
|
||||
mDescription = description;
|
||||
}
|
||||
|
||||
public String getDescription(){
|
||||
return mDescription;
|
||||
}
|
||||
|
||||
/** Set the cluster icon to be drawn when a cluster contains more than 1 marker.
|
||||
* If not set, default will be the default osmdroid marker icon (which is really inappropriate as a cluster icon). */
|
||||
public void setIcon(Bitmap icon){
|
||||
mClusterIcon = icon;
|
||||
}
|
||||
|
||||
/** Add the Marker.
|
||||
* Important: Markers added in a MarkerClusterer should not be added in the map overlays. */
|
||||
public void add(MarkerWithLabel marker){
|
||||
mItems.add(marker);
|
||||
}
|
||||
|
||||
/** Force a rebuild of clusters at next draw, even without a zooming action.
|
||||
* Should be done when you changed the content of a MarkerClusterer. */
|
||||
public void invalidate(){
|
||||
mLastZoomLevel = FORCE_CLUSTERING;
|
||||
}
|
||||
|
||||
/** @return the Marker at id (starting at 0) */
|
||||
public MarkerWithLabel getItem(int id){
|
||||
return mItems.get(id);
|
||||
}
|
||||
|
||||
/** @return the list of Markers. */
|
||||
public ArrayList<MarkerWithLabel> getItems(){
|
||||
return mItems;
|
||||
}
|
||||
|
||||
protected void hideInfoWindows(){
|
||||
for (MarkerWithLabel m : mItems){
|
||||
if (m.isInfoWindowShown())
|
||||
m.closeInfoWindow();
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void draw(Canvas canvas, MapView mapView, boolean shadow) {
|
||||
if (shadow)
|
||||
return;
|
||||
//if zoom has changed and mapView is now stable, rebuild clusters:
|
||||
int zoomLevel = mapView.getZoomLevel();
|
||||
if (zoomLevel != mLastZoomLevel && !mapView.isAnimating()){
|
||||
hideInfoWindows();
|
||||
mClusters = clusterer(mapView);
|
||||
renderer(mClusters, canvas, mapView);
|
||||
mLastZoomLevel = zoomLevel;
|
||||
}
|
||||
|
||||
for (StaticCluster cluster:mClusters){
|
||||
MarkerWithLabel marker = cluster.getMarker();
|
||||
marker.draw(canvas, mapView, false);
|
||||
}
|
||||
}
|
||||
|
||||
public Iterable<StaticCluster> reversedClusters() {
|
||||
return new Iterable<StaticCluster>() {
|
||||
@Override
|
||||
public Iterator<StaticCluster> iterator() {
|
||||
final ListIterator<StaticCluster> i = mClusters.listIterator(mClusters.size());
|
||||
return new Iterator<StaticCluster>() {
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return i.hasPrevious();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StaticCluster next() {
|
||||
return i.previous();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
i.remove();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override public boolean onSingleTapConfirmed(final MotionEvent event, final MapView mapView){
|
||||
for (final StaticCluster cluster : reversedClusters()) {
|
||||
if (cluster.getMarker().onSingleTapConfirmed(event, mapView))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override public boolean onLongPress(final MotionEvent event, final MapView mapView) {
|
||||
for (final StaticCluster cluster : reversedClusters()) {
|
||||
if (cluster.getMarker().onLongPress(event, mapView))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override public boolean onTouchEvent(final MotionEvent event, final MapView mapView) {
|
||||
for (StaticCluster cluster : reversedClusters()) {
|
||||
if (cluster.getMarker().onTouchEvent(event, mapView))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override public boolean onDoubleTap(final MotionEvent event, final MapView mapView) {
|
||||
for (final StaticCluster cluster : reversedClusters()) {
|
||||
if (cluster.getMarker().onDoubleTap(event, mapView))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override public BoundingBox getBounds(){
|
||||
if (mItems.size() == 0)
|
||||
return null;
|
||||
double minLat = Double.MAX_VALUE;
|
||||
double minLon = Double.MAX_VALUE;
|
||||
double maxLat = -Double.MAX_VALUE;
|
||||
double maxLon = -Double.MAX_VALUE;
|
||||
for (final MarkerWithLabel item : mItems) {
|
||||
final double latitude = item.getPosition().getLatitude();
|
||||
final double longitude = item.getPosition().getLongitude();
|
||||
minLat = Math.min(minLat, latitude);
|
||||
minLon = Math.min(minLon, longitude);
|
||||
maxLat = Math.max(maxLat, latitude);
|
||||
maxLon = Math.max(maxLon, longitude);
|
||||
}
|
||||
return new BoundingBox(maxLat, maxLon, minLat, minLon);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 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.cluster;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import org.meshtastic.app.map.model.MarkerWithLabel;
|
||||
|
||||
import org.osmdroid.bonuspack.R;
|
||||
import org.osmdroid.util.BoundingBox;
|
||||
import org.osmdroid.util.GeoPoint;
|
||||
import org.osmdroid.views.MapView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* Radius-based Clustering algorithm:
|
||||
* create a cluster using the first point from the cloned list.
|
||||
* All points that are found within the neighborhood are added to this cluster.
|
||||
* Then all the neighbors and the main point are removed from the list of points.
|
||||
* It continues until the list is empty.
|
||||
*
|
||||
* Largely inspired from GridMarkerClusterer by M.Kergall
|
||||
*
|
||||
* @author sidorovroman92@gmail.com
|
||||
*/
|
||||
|
||||
public class RadiusMarkerClusterer extends MarkerClusterer {
|
||||
|
||||
protected int mMaxClusteringZoomLevel = 7;
|
||||
protected int mRadiusInPixels = 100;
|
||||
protected double mRadiusInMeters;
|
||||
protected Paint mTextPaint;
|
||||
private ArrayList<MarkerWithLabel> mClonedMarkers;
|
||||
protected boolean mAnimated;
|
||||
int mDensityDpi;
|
||||
|
||||
/** cluster icon anchor */
|
||||
public float mAnchorU = MarkerWithLabel.ANCHOR_CENTER, mAnchorV = MarkerWithLabel.ANCHOR_CENTER;
|
||||
/** anchor point to draw the number of markers inside the cluster icon */
|
||||
public float mTextAnchorU = MarkerWithLabel.ANCHOR_CENTER, mTextAnchorV = MarkerWithLabel.ANCHOR_CENTER;
|
||||
|
||||
public RadiusMarkerClusterer(Context ctx) {
|
||||
super();
|
||||
mTextPaint = new Paint();
|
||||
mTextPaint.setColor(Color.WHITE);
|
||||
mTextPaint.setTextSize(15 * ctx.getResources().getDisplayMetrics().density);
|
||||
mTextPaint.setFakeBoldText(true);
|
||||
mTextPaint.setTextAlign(Paint.Align.CENTER);
|
||||
mTextPaint.setAntiAlias(true);
|
||||
Drawable clusterIconD = ctx.getResources().getDrawable(R.drawable.marker_cluster);
|
||||
Bitmap clusterIcon = ((BitmapDrawable) clusterIconD).getBitmap();
|
||||
setIcon(clusterIcon);
|
||||
mAnimated = true;
|
||||
mDensityDpi = ctx.getResources().getDisplayMetrics().densityDpi;
|
||||
}
|
||||
|
||||
/** If you want to change the default text paint (color, size, font) */
|
||||
public Paint getTextPaint(){
|
||||
return mTextPaint;
|
||||
}
|
||||
|
||||
/** Set the radius of clustering in pixels. Default is 100px. */
|
||||
public void setRadius(int radius){
|
||||
mRadiusInPixels = radius;
|
||||
}
|
||||
|
||||
/** Set max zoom level with clustering. When zoom is higher or equal to this level, clustering is disabled.
|
||||
* You can put a high value to disable this feature. */
|
||||
public void setMaxClusteringZoomLevel(int zoom){
|
||||
mMaxClusteringZoomLevel = zoom;
|
||||
}
|
||||
|
||||
/** Radius-Based clustering algorithm */
|
||||
@Override public ArrayList<StaticCluster> clusterer(MapView mapView) {
|
||||
|
||||
ArrayList<StaticCluster> clusters = new ArrayList<StaticCluster>();
|
||||
convertRadiusToMeters(mapView);
|
||||
|
||||
mClonedMarkers = new ArrayList<MarkerWithLabel>(mItems); //shallow copy
|
||||
while (!mClonedMarkers.isEmpty()) {
|
||||
MarkerWithLabel m = mClonedMarkers.get(0);
|
||||
StaticCluster cluster = createCluster(m, mapView);
|
||||
clusters.add(cluster);
|
||||
}
|
||||
return clusters;
|
||||
}
|
||||
|
||||
private StaticCluster createCluster(MarkerWithLabel m, MapView mapView) {
|
||||
GeoPoint clusterPosition = m.getPosition();
|
||||
|
||||
StaticCluster cluster = new StaticCluster(clusterPosition);
|
||||
cluster.add(m);
|
||||
|
||||
mClonedMarkers.remove(m);
|
||||
|
||||
if (mapView.getZoomLevel() > mMaxClusteringZoomLevel) {
|
||||
//above max level => block clustering:
|
||||
return cluster;
|
||||
}
|
||||
|
||||
Iterator<MarkerWithLabel> it = mClonedMarkers.iterator();
|
||||
while (it.hasNext()) {
|
||||
MarkerWithLabel neighbor = it.next();
|
||||
double distance = clusterPosition.distanceToAsDouble(neighbor.getPosition());
|
||||
if (distance <= mRadiusInMeters) {
|
||||
cluster.add(neighbor);
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
|
||||
return cluster;
|
||||
}
|
||||
|
||||
@Override public MarkerWithLabel buildClusterMarker(StaticCluster cluster, MapView mapView) {
|
||||
MarkerWithLabel m = new MarkerWithLabel(mapView, "", null);
|
||||
m.setPosition(cluster.getPosition());
|
||||
m.setInfoWindow(null);
|
||||
m.setAnchor(mAnchorU, mAnchorV);
|
||||
|
||||
Bitmap finalIcon = Bitmap.createBitmap(mClusterIcon.getScaledWidth(mDensityDpi),
|
||||
mClusterIcon.getScaledHeight(mDensityDpi), mClusterIcon.getConfig());
|
||||
Canvas iconCanvas = new Canvas(finalIcon);
|
||||
iconCanvas.drawBitmap(mClusterIcon, 0, 0, null);
|
||||
String text = "" + cluster.getSize();
|
||||
int textHeight = (int) (mTextPaint.descent() + mTextPaint.ascent());
|
||||
iconCanvas.drawText(text,
|
||||
mTextAnchorU * finalIcon.getWidth(),
|
||||
mTextAnchorV * finalIcon.getHeight() - textHeight / 2,
|
||||
mTextPaint);
|
||||
m.setIcon(new BitmapDrawable(mapView.getContext().getResources(), finalIcon));
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
@Override public void renderer(ArrayList<StaticCluster> clusters, Canvas canvas, MapView mapView) {
|
||||
for (StaticCluster cluster : clusters) {
|
||||
if (cluster.getSize() == 1) {
|
||||
//cluster has only 1 marker => use it as it is:
|
||||
cluster.setMarker(cluster.getItem(0));
|
||||
} else {
|
||||
//only draw 1 Marker at Cluster center, displaying number of Markers contained
|
||||
MarkerWithLabel m = buildClusterMarker(cluster, mapView);
|
||||
cluster.setMarker(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void convertRadiusToMeters(MapView mapView) {
|
||||
|
||||
Rect mScreenRect = mapView.getIntrinsicScreenRect(null);
|
||||
|
||||
int screenWidth = mScreenRect.right - mScreenRect.left;
|
||||
int screenHeight = mScreenRect.bottom - mScreenRect.top;
|
||||
|
||||
BoundingBox bb = mapView.getBoundingBox();
|
||||
|
||||
double diagonalInMeters = bb.getDiagonalLengthInMeters();
|
||||
double diagonalInPixels = Math.sqrt(screenWidth * screenWidth + screenHeight * screenHeight);
|
||||
double metersInPixel = diagonalInMeters / diagonalInPixels;
|
||||
|
||||
mRadiusInMeters = mRadiusInPixels * metersInPixel;
|
||||
}
|
||||
|
||||
public void setAnimation(boolean animate){
|
||||
mAnimated = animate;
|
||||
}
|
||||
|
||||
public void zoomOnCluster(MapView mapView, StaticCluster cluster){
|
||||
BoundingBox bb = cluster.getBoundingBox();
|
||||
if (bb.getLatNorth()!=bb.getLatSouth() || bb.getLonEast()!=bb.getLonWest()) {
|
||||
bb = bb.increaseByScale(2.3f);
|
||||
mapView.zoomToBoundingBox(bb, true);
|
||||
} else //all points exactly at the same place:
|
||||
mapView.setExpectedCenter(bb.getCenterWithDateLine());
|
||||
}
|
||||
|
||||
@Override public boolean onSingleTapConfirmed(final MotionEvent event, final MapView mapView){
|
||||
for (final StaticCluster cluster : reversedClusters()) {
|
||||
if (cluster.getMarker().onSingleTapConfirmed(event, mapView)) {
|
||||
if (mAnimated && cluster.getSize() > 1)
|
||||
zoomOnCluster(mapView, cluster);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 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.cluster;
|
||||
|
||||
import org.meshtastic.app.map.model.MarkerWithLabel;
|
||||
|
||||
import org.osmdroid.util.BoundingBox;
|
||||
import org.osmdroid.util.GeoPoint;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Cluster of Markers.
|
||||
* @author M.Kergall
|
||||
*/
|
||||
public class StaticCluster {
|
||||
protected final ArrayList<MarkerWithLabel> mItems = new ArrayList<MarkerWithLabel>();
|
||||
protected GeoPoint mCenter;
|
||||
protected MarkerWithLabel mMarker;
|
||||
|
||||
public StaticCluster(GeoPoint center) {
|
||||
mCenter = center;
|
||||
}
|
||||
|
||||
public void setPosition(GeoPoint center){
|
||||
mCenter = center;
|
||||
}
|
||||
|
||||
public GeoPoint getPosition() {
|
||||
return mCenter;
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return mItems.size();
|
||||
}
|
||||
|
||||
public MarkerWithLabel getItem(int index) {
|
||||
return mItems.get(index);
|
||||
}
|
||||
|
||||
public boolean add(MarkerWithLabel t) {
|
||||
return mItems.add(t);
|
||||
}
|
||||
|
||||
/** set the Marker to be displayed for this cluster */
|
||||
public void setMarker(MarkerWithLabel marker){
|
||||
mMarker = marker;
|
||||
}
|
||||
|
||||
/** @return the Marker to be displayed for this cluster */
|
||||
public MarkerWithLabel getMarker(){
|
||||
return mMarker;
|
||||
}
|
||||
|
||||
public BoundingBox getBoundingBox(){
|
||||
if (getSize()==0)
|
||||
return null;
|
||||
GeoPoint p = getItem(0).getPosition();
|
||||
BoundingBox bb = new BoundingBox(p.getLatitude(), p.getLongitude(), p.getLatitude(), p.getLongitude());
|
||||
for (int i=1; i<getSize(); i++) {
|
||||
p = getItem(i).getPosition();
|
||||
double minLat = Math.min(bb.getLatSouth(), p.getLatitude());
|
||||
double minLon = Math.min(bb.getLonWest(), p.getLongitude());
|
||||
double maxLat = Math.max(bb.getLatNorth(), p.getLatitude());
|
||||
double maxLon = Math.max(bb.getLonEast(), p.getLongitude());
|
||||
bb.set(maxLat, maxLon, minLat, minLon);
|
||||
}
|
||||
return bb;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 org.meshtastic.app.analytics
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import co.touchlab.kermit.Severity
|
||||
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 [PlatformAnalytics]. This provides no-op implementations for analytics and other
|
||||
* platform services.
|
||||
*/
|
||||
@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)." }
|
||||
} else {
|
||||
Logger.setMinSeverity(Severity.Info)
|
||||
Logger.i { "F-Droid platform no-op analytics initialized." }
|
||||
}
|
||||
}
|
||||
|
||||
override fun setDeviceAttributes(firmwareVersion: String, model: String) {
|
||||
// No-op for F-Droid
|
||||
Logger.d { "Set device attributes called: firmwareVersion=$firmwareVersion, deviceHardware=$model" }
|
||||
}
|
||||
|
||||
override val isPlatformServicesAvailable: Boolean
|
||||
get() = false
|
||||
|
||||
override fun track(event: String, vararg properties: DataPair) {
|
||||
Logger.d { "Track called: event=$event, properties=${properties.toList()}" }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +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 org.meshtastic.app.di
|
||||
|
||||
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.service.ApiService
|
||||
|
||||
@Module
|
||||
class FDroidNetworkModule {
|
||||
|
||||
@Single
|
||||
fun provideApiService(): ApiService = object : ApiService {
|
||||
override suspend fun getDeviceHardware(): List<NetworkDeviceHardware> =
|
||||
throw NotImplementedError("API calls to getDeviceHardware are not supported on Fdroid builds.")
|
||||
|
||||
override suspend fun getFirmwareReleases(): NetworkFirmwareReleases =
|
||||
throw NotImplementedError("API calls to getFirmwareReleases are not supported on Fdroid builds.")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
@Module(includes = [FDroidNetworkModule::class])
|
||||
class FlavorModule
|
||||
|
|
@ -1,24 +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 org.meshtastic.app.intro
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
fun AnalyticsIntro() {
|
||||
// no-op for fdroid
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* 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,80 +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 org.meshtastic.app.map
|
||||
|
||||
import android.content.Context
|
||||
import android.util.TypedValue
|
||||
import org.osmdroid.util.BoundingBox
|
||||
import org.osmdroid.util.GeoPoint
|
||||
import kotlin.math.log2
|
||||
import kotlin.math.pow
|
||||
|
||||
private const val DEGREES_IN_CIRCLE = 360.0
|
||||
private const val METERS_PER_DEGREE_LATITUDE = 111320.0
|
||||
private const val ZOOM_ADJUSTMENT_FACTOR = 0.8
|
||||
|
||||
/**
|
||||
* Calculates the zoom level required to fit the entire [BoundingBox] inside the map view.
|
||||
*
|
||||
* @return The zoom level as a Double value.
|
||||
*/
|
||||
fun BoundingBox.requiredZoomLevel(): Double {
|
||||
val topLeft = GeoPoint(this.latNorth, this.lonWest)
|
||||
val bottomRight = GeoPoint(this.latSouth, this.lonEast)
|
||||
val latLonWidth = topLeft.distanceToAsDouble(GeoPoint(topLeft.latitude, bottomRight.longitude))
|
||||
val latLonHeight = topLeft.distanceToAsDouble(GeoPoint(bottomRight.latitude, topLeft.longitude))
|
||||
val requiredLatZoom = log2(DEGREES_IN_CIRCLE / (latLonHeight / METERS_PER_DEGREE_LATITUDE))
|
||||
val requiredLonZoom = log2(DEGREES_IN_CIRCLE / (latLonWidth / METERS_PER_DEGREE_LATITUDE))
|
||||
return maxOf(requiredLatZoom, requiredLonZoom) * ZOOM_ADJUSTMENT_FACTOR
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new bounding box with adjusted dimensions based on the provided [zoomFactor].
|
||||
*
|
||||
* @return A new [BoundingBox] with added [zoomFactor]. Example:
|
||||
* ```
|
||||
* // Setting the zoom level directly using setZoom()
|
||||
* map.setZoom(14.0)
|
||||
* val boundingBoxZoom14 = map.boundingBox
|
||||
*
|
||||
* // Using zoomIn() results the equivalent BoundingBox with setZoom(15.0)
|
||||
* val boundingBoxZoom15 = boundingBoxZoom14.zoomIn(1.0)
|
||||
* ```
|
||||
*/
|
||||
fun BoundingBox.zoomIn(zoomFactor: Double): BoundingBox {
|
||||
val center = GeoPoint((latNorth + latSouth) / 2, (lonWest + lonEast) / 2)
|
||||
val latDiff = latNorth - latSouth
|
||||
val lonDiff = lonEast - lonWest
|
||||
|
||||
val newLatDiff = latDiff / (2.0.pow(zoomFactor))
|
||||
val newLonDiff = lonDiff / (2.0.pow(zoomFactor))
|
||||
|
||||
return BoundingBox(
|
||||
center.latitude + newLatDiff / 2,
|
||||
center.longitude + newLonDiff / 2,
|
||||
center.latitude - newLatDiff / 2,
|
||||
center.longitude - newLonDiff / 2,
|
||||
)
|
||||
}
|
||||
|
||||
// Converts SP to pixels.
|
||||
fun Context.spToPx(sp: Float): Int =
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, resources.displayMetrics).toInt()
|
||||
|
||||
// Converts DP to pixels.
|
||||
fun Context.dpToPx(dp: Float): Int =
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics).toInt()
|
||||
|
|
@ -1,962 +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 org.meshtastic.app.map
|
||||
|
||||
import android.Manifest
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
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.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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
|
||||
import androidx.compose.runtime.Composable
|
||||
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
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
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.model.DataPacket
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.calculating
|
||||
import org.meshtastic.core.resources.cancel
|
||||
import org.meshtastic.core.resources.clear
|
||||
import org.meshtastic.core.resources.close
|
||||
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.last_heard_filter_label
|
||||
import org.meshtastic.core.resources.location_disabled
|
||||
import org.meshtastic.core.resources.map_cache_info
|
||||
import org.meshtastic.core.resources.map_cache_manager
|
||||
import org.meshtastic.core.resources.map_cache_size
|
||||
import org.meshtastic.core.resources.map_cache_tiles
|
||||
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_node_popup_details
|
||||
import org.meshtastic.core.resources.map_offline_manager
|
||||
import org.meshtastic.core.resources.map_purge_fail
|
||||
import org.meshtastic.core.resources.map_purge_success
|
||||
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.show_precision_circle
|
||||
import org.meshtastic.core.resources.show_waypoints
|
||||
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.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.BaseMapViewModel.MapFilterState
|
||||
import org.meshtastic.feature.map.LastHeardFilter
|
||||
import org.meshtastic.feature.map.component.MapButton
|
||||
import org.meshtastic.feature.map.component.MapControlsOverlay
|
||||
import org.meshtastic.proto.Config.DisplayConfig.DisplayUnits
|
||||
import org.meshtastic.proto.Waypoint
|
||||
import org.osmdroid.bonuspack.utils.BonusPackHelper.getBitmapFromVectorDrawable
|
||||
import org.osmdroid.config.Configuration
|
||||
import org.osmdroid.events.MapEventsReceiver
|
||||
import org.osmdroid.events.MapListener
|
||||
import org.osmdroid.events.ScrollEvent
|
||||
import org.osmdroid.events.ZoomEvent
|
||||
import org.osmdroid.tileprovider.cachemanager.CacheManager
|
||||
import org.osmdroid.tileprovider.modules.SqliteArchiveTileWriter
|
||||
import org.osmdroid.tileprovider.tilesource.ITileSource
|
||||
import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase
|
||||
import org.osmdroid.tileprovider.tilesource.TileSourcePolicyException
|
||||
import org.osmdroid.util.BoundingBox
|
||||
import org.osmdroid.util.GeoPoint
|
||||
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.infowindow.InfoWindow
|
||||
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
|
||||
import java.io.File
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private fun MapView.updateMarkers(
|
||||
nodeMarkers: List<MarkerWithLabel>,
|
||||
waypointMarkers: List<MarkerWithLabel>,
|
||||
nodeClusterer: RadiusMarkerClusterer,
|
||||
) {
|
||||
Logger.d { "Showing on map: ${nodeMarkers.size} nodes ${waypointMarkers.size} waypoints" }
|
||||
|
||||
overlays.removeAll { overlay ->
|
||||
overlay is MarkerWithLabel || (overlay is Marker && overlay !in nodeClusterer.items)
|
||||
}
|
||||
|
||||
overlays.addAll(waypointMarkers)
|
||||
|
||||
nodeClusterer.items.clear()
|
||||
nodeClusterer.items.addAll(nodeMarkers)
|
||||
nodeClusterer.invalidate()
|
||||
}
|
||||
|
||||
private fun cacheManagerCallback(onTaskComplete: () -> Unit, onTaskFailed: (Int) -> Unit) =
|
||||
object : CacheManager.CacheManagerCallback {
|
||||
override fun onTaskComplete() {
|
||||
onTaskComplete()
|
||||
}
|
||||
|
||||
override fun onTaskFailed(errors: Int) {
|
||||
onTaskFailed(errors)
|
||||
}
|
||||
|
||||
override fun updateProgress(progress: Int, currentZoomLevel: Int, zoomMin: Int, zoomMax: Int) {
|
||||
// NOOP since we are using the build in UI
|
||||
}
|
||||
|
||||
override fun downloadStarted() {
|
||||
// NOOP since we are using the build in UI
|
||||
}
|
||||
|
||||
override fun setPossibleTilesInArea(total: Int) {
|
||||
// NOOP since we are using the build in UI
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main composable for displaying the map view, including nodes, waypoints, and user location. It handles user
|
||||
* interactions for map manipulation, filtering, and offline caching.
|
||||
*
|
||||
* @param mapViewModel The [MapViewModel] providing data and state for the map.
|
||||
* @param navigateToNodeDetails Callback to navigate to the details screen of a selected node.
|
||||
*/
|
||||
@OptIn(ExperimentalPermissionsApi::class) // Added for Accompanist
|
||||
@Suppress("CyclomaticComplexMethod", "LongMethod")
|
||||
@Composable
|
||||
fun MapView(
|
||||
modifier: Modifier = Modifier,
|
||||
mapViewModel: MapViewModel = koinViewModel(),
|
||||
navigateToNodeDetails: (Int) -> Unit,
|
||||
) {
|
||||
var mapFilterExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val mapFilterState by mapViewModel.mapFilterStateFlow.collectAsStateWithLifecycle()
|
||||
val isConnected by mapViewModel.isConnected.collectAsStateWithLifecycle()
|
||||
|
||||
var cacheEstimate by remember { mutableStateOf("") }
|
||||
|
||||
var zoomLevelMin by remember { mutableDoubleStateOf(0.0) }
|
||||
var zoomLevelMax by remember { mutableDoubleStateOf(0.0) }
|
||||
|
||||
var downloadRegionBoundingBox: BoundingBox? by remember { mutableStateOf(null) }
|
||||
var myLocationOverlay: MyLocationNewOverlay? by remember { mutableStateOf(null) }
|
||||
|
||||
var showDownloadButton: Boolean by remember { mutableStateOf(false) }
|
||||
var showEditWaypointDialog by remember { mutableStateOf<Waypoint?>(null) }
|
||||
var showCacheManagerDialog by remember { mutableStateOf(false) }
|
||||
var showCurrentCacheInfo by remember { mutableStateOf(false) }
|
||||
var showPurgeTileSourceDialog by remember { mutableStateOf(false) }
|
||||
var showMapStyleDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val density = LocalDensity.current
|
||||
|
||||
val haptic = LocalHapticFeedback.current
|
||||
fun performHapticFeedback() = haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
|
||||
// Accompanist permissions state for location
|
||||
val locationPermissionsState =
|
||||
rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION))
|
||||
var triggerLocationToggleAfterPermission by remember { mutableStateOf(false) }
|
||||
|
||||
fun loadOnlineTileSourceBase(): ITileSource {
|
||||
val id = mapViewModel.mapStyleId
|
||||
Logger.d { "mapStyleId from prefs: $id" }
|
||||
return CustomTileSource.getTileSource(id).also {
|
||||
zoomLevelMax = it.maximumZoomLevel.toDouble()
|
||||
showDownloadButton = if (it is OnlineTileSourceBase) it.tileSourcePolicy.acceptsBulkDownload() else false
|
||||
}
|
||||
}
|
||||
|
||||
val initialCameraView = remember {
|
||||
val nodes = mapViewModel.nodes.value
|
||||
val nodesWithPosition = nodes.filter { it.validPosition != null }
|
||||
val geoPoints = nodesWithPosition.map { GeoPoint(it.latitude, it.longitude) }
|
||||
BoundingBox.fromGeoPoints(geoPoints)
|
||||
}
|
||||
val map =
|
||||
rememberMapViewWithLifecycle(
|
||||
applicationId = mapViewModel.applicationId,
|
||||
box = initialCameraView,
|
||||
tileSource = loadOnlineTileSourceBase(),
|
||||
)
|
||||
|
||||
val nodeClusterer = remember { RadiusMarkerClusterer(context) }
|
||||
|
||||
fun MapView.toggleMyLocation() {
|
||||
if (context.gpsDisabled()) {
|
||||
Logger.d { "Telling user we need location turned on for MyLocationNewOverlay" }
|
||||
scope.launch { context.showToast(Res.string.location_disabled) }
|
||||
return
|
||||
}
|
||||
|
||||
Logger.d { "user clicked MyLocationNewOverlay ${myLocationOverlay == null}" }
|
||||
if (myLocationOverlay == null) {
|
||||
myLocationOverlay =
|
||||
MyLocationNewOverlay(this).apply {
|
||||
enableMyLocation()
|
||||
enableFollowLocation()
|
||||
getBitmapFromVectorDrawable(context, R.drawable.ic_map_location_dot)?.let {
|
||||
setPersonIcon(it)
|
||||
setPersonAnchor(0.5f, 0.5f)
|
||||
}
|
||||
getBitmapFromVectorDrawable(context, R.drawable.ic_map_navigation)?.let {
|
||||
setDirectionIcon(it)
|
||||
setDirectionAnchor(0.5f, 0.5f)
|
||||
}
|
||||
}
|
||||
overlays.add(myLocationOverlay)
|
||||
} else {
|
||||
myLocationOverlay?.apply {
|
||||
disableMyLocation()
|
||||
disableFollowLocation()
|
||||
}
|
||||
overlays.remove(myLocationOverlay)
|
||||
myLocationOverlay = null
|
||||
}
|
||||
}
|
||||
|
||||
// Effect to toggle MyLocation after permission is granted
|
||||
LaunchedEffect(locationPermissionsState.allPermissionsGranted) {
|
||||
if (locationPermissionsState.allPermissionsGranted && triggerLocationToggleAfterPermission) {
|
||||
map.toggleMyLocation()
|
||||
triggerLocationToggleAfterPermission = false
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
val myId by mapViewModel.myId.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(selectedWaypointId, waypoints) {
|
||||
if (selectedWaypointId != null && waypoints.containsKey(selectedWaypointId)) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 ?: 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.lastHeardFilter.seconds != 0L &&
|
||||
(nowSeconds - node.lastHeard) > mapFilterStateValue.lastHeardFilter.seconds &&
|
||||
node.num != ourNode?.num
|
||||
) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
val (p, u) = node.position to node.user
|
||||
val nodePosition = GeoPoint(node.latitude, node.longitude)
|
||||
MarkerWithLabel(mapView = this, label = "${u.short_name} ${formatAgo(p.time)}").apply {
|
||||
id = u.id
|
||||
title = u.long_name
|
||||
snippet =
|
||||
getString(
|
||||
Res.string.map_node_popup_details,
|
||||
node.gpsString(),
|
||||
formatAgo(node.lastHeard),
|
||||
formatAgo(p.time),
|
||||
if (node.batteryStr != "") node.batteryStr else "?",
|
||||
)
|
||||
ourNode?.distanceStr(node, displayUnits)?.let { dist ->
|
||||
ourNode.bearing(node)?.let { bearing ->
|
||||
subDescription = getString(Res.string.map_subDescription, bearing, dist)
|
||||
}
|
||||
}
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||
position = nodePosition
|
||||
icon = markerIcon
|
||||
setNodeColors(node.colors)
|
||||
if (!mapFilterStateValue.showPrecisionCircle) {
|
||||
setPrecisionBits(0)
|
||||
} else {
|
||||
setPrecisionBits(p.precision_bits)
|
||||
}
|
||||
setOnLongClickListener {
|
||||
navigateToNodeDetails(node.num)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showDeleteMarkerDialog(waypoint: Waypoint) {
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
builder.setTitle(getString(Res.string.waypoint_delete))
|
||||
builder.setNeutralButton(getString(Res.string.cancel)) { _, _ ->
|
||||
Logger.d { "User canceled marker delete dialog" }
|
||||
}
|
||||
builder.setNegativeButton(getString(Res.string.delete_for_me)) { _, _ ->
|
||||
Logger.d { "User deleted waypoint ${waypoint.id} for me" }
|
||||
mapViewModel.deleteWaypoint(waypoint.id)
|
||||
}
|
||||
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))
|
||||
mapViewModel.deleteWaypoint(waypoint.id)
|
||||
}
|
||||
}
|
||||
val dialog = builder.show()
|
||||
for (
|
||||
button in
|
||||
setOf(
|
||||
androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL,
|
||||
androidx.appcompat.app.AlertDialog.BUTTON_NEGATIVE,
|
||||
androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE,
|
||||
)
|
||||
) {
|
||||
with(dialog.getButton(button)) {
|
||||
textSize = 12F
|
||||
isAllCaps = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showMarkerLongPressDialog(id: Int) {
|
||||
performHapticFeedback()
|
||||
Logger.d { "marker long pressed id=$id" }
|
||||
val waypoint = waypoints[id]?.waypoint ?: return
|
||||
// edit only when unlocked or lockedTo myNodeNum
|
||||
if (waypoint.locked_to in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) {
|
||||
showEditWaypointDialog = waypoint
|
||||
} else {
|
||||
showDeleteMarkerDialog(waypoint)
|
||||
}
|
||||
}
|
||||
|
||||
fun getUsername(id: String?) = if (id == DataPacket.ID_LOCAL || (myId != null && id == myId)) {
|
||||
getString(Res.string.you)
|
||||
} else {
|
||||
mapViewModel.getUser(id).long_name
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun MapView.onWaypointChanged(waypoints: Collection<DataPacket>, selectedWaypointId: Int?): List<MarkerWithLabel> {
|
||||
return waypoints.mapNotNull { waypoint ->
|
||||
val pt = waypoint.waypoint ?: return@mapNotNull null
|
||||
if (!mapFilterState.showWaypoints) return@mapNotNull null // Use collected mapFilterState
|
||||
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 * 1000L
|
||||
val expireTimeStr =
|
||||
when {
|
||||
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.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) {
|
||||
showInfoWindow()
|
||||
}
|
||||
setOnLongClickListener {
|
||||
showMarkerLongPressDialog(pt.id)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val mapEventsReceiver =
|
||||
object : MapEventsReceiver {
|
||||
override fun singleTapConfirmedHelper(p: GeoPoint): Boolean {
|
||||
InfoWindow.closeAllInfoWindowsOn(map)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun longPressHelper(p: GeoPoint): Boolean {
|
||||
performHapticFeedback()
|
||||
val enabled = isConnected && downloadRegionBoundingBox == null
|
||||
|
||||
if (enabled) {
|
||||
showEditWaypointDialog =
|
||||
Waypoint(latitude_i = (p.latitude * 1e7).toInt(), longitude_i = (p.longitude * 1e7).toInt())
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
fun MapView.drawOverlays() {
|
||||
if (overlays.none { it is MapEventsOverlay }) {
|
||||
overlays.add(0, MapEventsOverlay(mapEventsReceiver))
|
||||
}
|
||||
if (myLocationOverlay != null && overlays.none { it is MyLocationNewOverlay }) {
|
||||
overlays.add(myLocationOverlay)
|
||||
}
|
||||
if (overlays.none { it is RadiusMarkerClusterer }) {
|
||||
overlays.add(nodeClusterer)
|
||||
}
|
||||
|
||||
addCopyright()
|
||||
addScaleBarOverlay(density)
|
||||
createLatLongGrid(false)
|
||||
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun MapView.generateBoxOverlay() {
|
||||
overlays.removeAll { it is Polygon }
|
||||
val zoomFactor = 1.3
|
||||
zoomLevelMin = minOf(zoomLevelDouble, zoomLevelMax)
|
||||
downloadRegionBoundingBox = boundingBox.zoomIn(zoomFactor)
|
||||
val polygon =
|
||||
Polygon().apply {
|
||||
points = Polygon.pointsAsRect(downloadRegionBoundingBox).map { GeoPoint(it.latitude, it.longitude) }
|
||||
}
|
||||
overlays.add(polygon)
|
||||
invalidate()
|
||||
val tileCount: Int =
|
||||
CacheManager(this)
|
||||
.possibleTilesInArea(downloadRegionBoundingBox, zoomLevelMin.toInt(), zoomLevelMax.toInt())
|
||||
cacheEstimate = getString(Res.string.map_cache_tiles, tileCount)
|
||||
}
|
||||
|
||||
val boxOverlayListener =
|
||||
object : MapListener {
|
||||
override fun onScroll(event: ScrollEvent): Boolean {
|
||||
if (downloadRegionBoundingBox != null) {
|
||||
event.source.generateBoxOverlay()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onZoom(event: ZoomEvent): Boolean = false
|
||||
}
|
||||
|
||||
fun startDownload() {
|
||||
val boundingBox = downloadRegionBoundingBox ?: return
|
||||
try {
|
||||
val outputName = buildString {
|
||||
append(Configuration.getInstance().osmdroidBasePath.absolutePath)
|
||||
append(File.separator)
|
||||
append("mainFile.sqlite")
|
||||
}
|
||||
val writer = SqliteArchiveTileWriter(outputName)
|
||||
val cacheManager = CacheManager(map, writer)
|
||||
cacheManager.downloadAreaAsync(
|
||||
context,
|
||||
boundingBox,
|
||||
zoomLevelMin.toInt(),
|
||||
zoomLevelMax.toInt(),
|
||||
cacheManagerCallback(
|
||||
onTaskComplete = {
|
||||
scope.launch { context.showToast(Res.string.map_download_complete) }
|
||||
writer.onDetach()
|
||||
},
|
||||
onTaskFailed = { errors ->
|
||||
scope.launch { context.showToast(Res.string.map_download_errors, errors) }
|
||||
writer.onDetach()
|
||||
},
|
||||
),
|
||||
)
|
||||
} catch (ex: TileSourcePolicyException) {
|
||||
Logger.d { "Tile source does not allow archiving: ${ex.message}" }
|
||||
} catch (ex: Exception) {
|
||||
Logger.d { "Tile source exception: ${ex.message}" }
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
floatingActionButton = {
|
||||
DownloadButton(showDownloadButton && downloadRegionBoundingBox == null) { showCacheManagerDialog = true }
|
||||
},
|
||||
) { innerPadding ->
|
||||
Box(modifier = Modifier.fillMaxSize().padding(innerPadding)) {
|
||||
AndroidView(
|
||||
factory = {
|
||||
map.apply {
|
||||
setDestroyMode(false)
|
||||
addMapListener(boxOverlayListener)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
update = { mapView ->
|
||||
with(mapView) {
|
||||
updateMarkers(
|
||||
onNodesChanged(nodes),
|
||||
onWaypointChanged(waypoints.values, selectedWaypointId),
|
||||
nodeClusterer,
|
||||
)
|
||||
}
|
||||
mapView.drawOverlays()
|
||||
}, // Renamed map to mapView to avoid conflict
|
||||
)
|
||||
if (downloadRegionBoundingBox != null) {
|
||||
CacheLayout(
|
||||
cacheEstimate = cacheEstimate,
|
||||
onExecuteJob = { startDownload() },
|
||||
onCancelDownload = {
|
||||
downloadRegionBoundingBox = null
|
||||
map.overlays.removeAll { it is Polygon }
|
||||
map.invalidate()
|
||||
},
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
)
|
||||
} else {
|
||||
MapControlsOverlay(
|
||||
modifier = Modifier.align(Alignment.TopCenter).padding(top = 8.dp),
|
||||
onToggleFilterMenu = { mapFilterExpanded = true },
|
||||
filterDropdownContent = {
|
||||
FdroidMainMapFilterDropdown(
|
||||
expanded = mapFilterExpanded,
|
||||
onDismissRequest = { mapFilterExpanded = false },
|
||||
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()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showMapStyleDialog) {
|
||||
MapStyleDialog(
|
||||
selectedMapStyle = mapViewModel.mapStyleId,
|
||||
onDismiss = { showMapStyleDialog = false },
|
||||
onSelectMapStyle = {
|
||||
mapViewModel.mapStyleId = it
|
||||
map.setTileSource(loadOnlineTileSourceBase())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (showCacheManagerDialog) {
|
||||
CacheManagerDialog(
|
||||
onClickOption = { option ->
|
||||
when (option) {
|
||||
CacheManagerOption.CurrentCacheSize -> {
|
||||
scope.launch { context.showToast(Res.string.calculating) }
|
||||
showCurrentCacheInfo = true
|
||||
}
|
||||
CacheManagerOption.DownloadRegion -> map.generateBoxOverlay()
|
||||
|
||||
CacheManagerOption.ClearTiles -> showPurgeTileSourceDialog = true
|
||||
CacheManagerOption.Cancel -> Unit
|
||||
}
|
||||
showCacheManagerDialog = false
|
||||
},
|
||||
onDismiss = { showCacheManagerDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showCurrentCacheInfo) {
|
||||
CacheInfoDialog(mapView = map, onDismiss = { showCurrentCacheInfo = false })
|
||||
}
|
||||
|
||||
if (showPurgeTileSourceDialog) {
|
||||
PurgeTileSourceDialog(onDismiss = { showPurgeTileSourceDialog = false })
|
||||
}
|
||||
|
||||
if (showEditWaypointDialog != null) {
|
||||
EditWaypointDialog(
|
||||
waypoint = showEditWaypointDialog ?: return, // Safe call
|
||||
onSendClicked = { waypoint ->
|
||||
Logger.d { "User clicked send waypoint ${waypoint.id}" }
|
||||
showEditWaypointDialog = null
|
||||
|
||||
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) 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(
|
||||
id = newId,
|
||||
name = newName,
|
||||
expire = newExpire,
|
||||
locked_to = newLockedTo,
|
||||
icon = newIcon,
|
||||
),
|
||||
)
|
||||
},
|
||||
onDeleteClicked = { waypoint ->
|
||||
Logger.d { "User clicked delete waypoint ${waypoint.id}" }
|
||||
showEditWaypointDialog = null
|
||||
showDeleteMarkerDialog(waypoint)
|
||||
},
|
||||
onDismissRequest = {
|
||||
Logger.d { "User clicked cancel marker edit dialog" }
|
||||
showEditWaypointDialog = null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** 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) }
|
||||
|
||||
MapsDialog(onDismiss = onDismiss) {
|
||||
CustomTileSource.mTileSources.values.forEachIndexed { index, style ->
|
||||
ListItem(
|
||||
text = style,
|
||||
trailingIcon = if (index == selected.value) MeshtasticIcons.Check else null,
|
||||
onClick = {
|
||||
selected.value = index
|
||||
onSelectMapStyle(index)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class CacheManagerOption(val label: StringResource) {
|
||||
CurrentCacheSize(label = Res.string.map_cache_size),
|
||||
DownloadRegion(label = Res.string.map_download_region),
|
||||
ClearTiles(label = Res.string.map_clear_tiles),
|
||||
Cancel(label = Res.string.cancel),
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CacheManagerDialog(onClickOption: (CacheManagerOption) -> Unit, onDismiss: () -> Unit) {
|
||||
MapsDialog(title = stringResource(Res.string.map_offline_manager), onDismiss = onDismiss) {
|
||||
CacheManagerOption.entries.forEach { option ->
|
||||
ListItem(text = stringResource(option.label), trailingIcon = null) {
|
||||
onClickOption(option)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CacheInfoDialog(mapView: MapView, onDismiss: () -> Unit) {
|
||||
val (cacheCapacity, currentCacheUsage) =
|
||||
remember(mapView) {
|
||||
val cacheManager = CacheManager(mapView)
|
||||
cacheManager.cacheCapacity() to cacheManager.currentCacheUsage()
|
||||
}
|
||||
|
||||
MapsDialog(
|
||||
title = stringResource(Res.string.map_cache_manager),
|
||||
onDismiss = onDismiss,
|
||||
negativeButton = { TextButton(onClick = { onDismiss() }) { Text(text = stringResource(Res.string.close)) } },
|
||||
) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PurgeTileSourceDialog(onDismiss: () -> Unit) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val cache = SqlTileWriterExt()
|
||||
|
||||
val sourceList by derivedStateOf { cache.sources.map { it.source as String } }
|
||||
|
||||
val selected = remember { mutableStateListOf<Int>() }
|
||||
|
||||
MapsDialog(
|
||||
title = stringResource(Res.string.map_tile_source),
|
||||
positiveButton = {
|
||||
TextButton(
|
||||
enabled = selected.isNotEmpty(),
|
||||
onClick = {
|
||||
selected.forEach { selectedIndex ->
|
||||
val source = sourceList[selectedIndex]
|
||||
scope.launch {
|
||||
context.showToast(
|
||||
if (cache.purgeCache(source)) {
|
||||
getString(Res.string.map_purge_success, source)
|
||||
} else {
|
||||
getString(Res.string.map_purge_fail)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onDismiss()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(Res.string.clear))
|
||||
}
|
||||
},
|
||||
negativeButton = { TextButton(onClick = onDismiss) { Text(text = stringResource(Res.string.cancel)) } },
|
||||
onDismiss = onDismiss,
|
||||
) {
|
||||
sourceList.forEachIndexed { index, source ->
|
||||
val isSelected = selected.contains(index)
|
||||
BasicListItem(
|
||||
text = source,
|
||||
trailingContent = { Checkbox(checked = isSelected, onCheckedChange = {}) },
|
||||
onClick = {
|
||||
if (isSelected) {
|
||||
selected.remove(index)
|
||||
} else {
|
||||
selected.add(index)
|
||||
}
|
||||
},
|
||||
) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun MapsDialog(
|
||||
title: String? = null,
|
||||
onDismiss: () -> Unit,
|
||||
positiveButton: (@Composable () -> Unit)? = null,
|
||||
negativeButton: (@Composable () -> Unit)? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
BasicAlertDialog(onDismissRequest = onDismiss) {
|
||||
Surface(
|
||||
modifier = Modifier.wrapContentWidth().wrapContentHeight(),
|
||||
shape = MaterialTheme.shapes.large,
|
||||
color = AlertDialogDefaults.containerColor,
|
||||
tonalElevation = AlertDialogDefaults.TonalElevation,
|
||||
) {
|
||||
Column {
|
||||
title?.let {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 8.dp),
|
||||
text = it,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { content() }
|
||||
if (positiveButton != null || negativeButton != null) {
|
||||
Row(Modifier.align(Alignment.End)) {
|
||||
positiveButton?.invoke()
|
||||
negativeButton?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val WAYPOINT_ZOOM = 15.0
|
||||
|
|
@ -1,145 +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 org.meshtastic.app.map
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.DashPathEffect
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Typeface
|
||||
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
|
||||
import org.osmdroid.views.overlay.CopyrightOverlay
|
||||
import org.osmdroid.views.overlay.Marker
|
||||
import org.osmdroid.views.overlay.Polyline
|
||||
import org.osmdroid.views.overlay.ScaleBarOverlay
|
||||
import org.osmdroid.views.overlay.advancedpolyline.MonochromaticPaintList
|
||||
import org.osmdroid.views.overlay.gridlines.LatLonGridlineOverlay2
|
||||
|
||||
/** Adds copyright to map depending on what source is showing */
|
||||
fun MapView.addCopyright() {
|
||||
if (overlays.none { it is CopyrightOverlay }) {
|
||||
val copyrightNotice: String = tileProvider.tileSource.copyrightNotice ?: return
|
||||
val copyrightOverlay = CopyrightOverlay(context)
|
||||
copyrightOverlay.setCopyrightNotice(copyrightNotice)
|
||||
overlays.add(copyrightOverlay)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create LatLong Grid line overlay
|
||||
*
|
||||
* @param enabled: turn on/off gridlines
|
||||
*/
|
||||
fun MapView.createLatLongGrid(enabled: Boolean) {
|
||||
val latLongGridOverlay = LatLonGridlineOverlay2()
|
||||
latLongGridOverlay.isEnabled = enabled
|
||||
if (latLongGridOverlay.isEnabled) {
|
||||
val textPaint =
|
||||
Paint().apply {
|
||||
textSize = 40f
|
||||
color = Color.GRAY
|
||||
isAntiAlias = true
|
||||
isFakeBoldText = true
|
||||
textAlign = Paint.Align.CENTER
|
||||
}
|
||||
latLongGridOverlay.textPaint = textPaint
|
||||
latLongGridOverlay.setBackgroundColor(Color.TRANSPARENT)
|
||||
latLongGridOverlay.setLineWidth(3.0f)
|
||||
latLongGridOverlay.setLineColor(Color.GRAY)
|
||||
overlays.add(latLongGridOverlay)
|
||||
}
|
||||
}
|
||||
|
||||
fun MapView.addScaleBarOverlay(density: Density) {
|
||||
if (overlays.none { it is ScaleBarOverlay }) {
|
||||
val scaleBarOverlay =
|
||||
ScaleBarOverlay(this).apply {
|
||||
setAlignBottom(true)
|
||||
with(density) {
|
||||
setScaleBarOffset(15.dp.toPx().toInt(), 40.dp.toPx().toInt())
|
||||
setTextSize(12.sp.toPx())
|
||||
}
|
||||
textPaint.apply {
|
||||
isAntiAlias = true
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
}
|
||||
}
|
||||
overlays.add(scaleBarOverlay)
|
||||
}
|
||||
}
|
||||
|
||||
fun MapView.addPolyline(density: Density, geoPoints: List<GeoPoint>, onClick: () -> Unit): Polyline {
|
||||
val polyline =
|
||||
Polyline(this).apply {
|
||||
val borderPaint =
|
||||
Paint().apply {
|
||||
color = Color.BLACK
|
||||
isAntiAlias = true
|
||||
strokeWidth = with(density) { 10.dp.toPx() }
|
||||
style = Paint.Style.STROKE
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
pathEffect = DashPathEffect(floatArrayOf(80f, 60f), 0f)
|
||||
}
|
||||
outlinePaintLists.add(MonochromaticPaintList(borderPaint))
|
||||
val fillPaint =
|
||||
Paint().apply {
|
||||
color = Color.WHITE
|
||||
isAntiAlias = true
|
||||
strokeWidth = with(density) { 6.dp.toPx() }
|
||||
style = Paint.Style.FILL_AND_STROKE
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
pathEffect = DashPathEffect(floatArrayOf(80f, 60f), 0f)
|
||||
}
|
||||
outlinePaintLists.add(MonochromaticPaintList(fillPaint))
|
||||
setPoints(geoPoints)
|
||||
setOnClickListener { _, _, _ ->
|
||||
onClick()
|
||||
true
|
||||
}
|
||||
}
|
||||
overlays.add(polyline)
|
||||
|
||||
return polyline
|
||||
}
|
||||
|
||||
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 { pos ->
|
||||
Marker(this).apply {
|
||||
icon = navIcon
|
||||
rotation = ((pos.ground_track ?: 0) * 1e-5).toFloat()
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
|
||||
position = GeoPoint((pos.latitude_i ?: 0) * 1e-7, (pos.longitude_i ?: 0) * 1e-7)
|
||||
setOnMarkerClickListener { _, _ ->
|
||||
onClick(pos.time)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
overlays.addAll(markers)
|
||||
|
||||
return markers
|
||||
}
|
||||
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