mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-05-24 14:50:26 -04:00
Merge remote-tracking branch 'origin/main' into features/lockdown-v2
# Conflicts: # .specify/feature.json # AGENTS.md # core/proto/src/main/proto
This commit is contained in:
@@ -3,6 +3,50 @@
|
||||
# Do NOT edit or remove previous entries — stale state claims cause agent confusion.
|
||||
# Format: ## YYYY-MM-DD — <summary>
|
||||
|
||||
## 2026-05-20 — Overhauled and Bulletproofed Flatpak Source Generation
|
||||
- Overhauled and streamlined `build-logic/convention/build.gradle.kts` to dynamically query and resolve all 25+ Version Catalog plugin marker coordinates in a type-safe dynamic loop.
|
||||
- Replaced the hardcoded embedded Gradle Kotlin compiler version with dynamic standard library detection: `KotlinVersion.CURRENT.toString()`.
|
||||
- Implemented root Gradle consolidator task `:combineFlatpakSources` using JVM-native Json Slurper/Output, deduplicating 2222 entries and injecting Google/Aliyun backup `mirror-urls`.
|
||||
- Streamlined reusable-check and release CI workflows, replacing complex `jq` hacks with a simple, single gradle task invocation.
|
||||
- Verified `spotlessCheck`, `detekt`, and end-to-end completely offline package builds (`--offline`) completed with 100% success.
|
||||
|
||||
## 2026-05-20 — Refactored and polished Flatpak dependency manifests to modern Gradle standards
|
||||
- Polished the Flatpak generator tasks and convention setup following a comprehensive audit:
|
||||
1. Centralized version definitions to reference the central `libs.versions` catalog (`kotlin`, `koin.plugin`, etc.) instead of hardcoded strings.
|
||||
2. Documented every single dependency override and compiler-plugin helper with clear, inline comments (`// why: ...`).
|
||||
3. Cleaned out legacy dependencies and streamlined `core:database` to only capture `kspKotlinJvmProcessorClasspath`.
|
||||
4. Changed includeConfigurations to use type-safe `setOf` instead of `listOf` to align with Gradle's `SetProperty` APIs.
|
||||
- Verified that all static analysis checks pass successfully: `./gradlew spotlessCheck detekt` is 100% green.
|
||||
- Validated end-to-end correctness by successfully compiling the app completely offline (`--offline`) and packaging the release UberJar.
|
||||
|
||||
## 2026-05-20 — Fixed Jekyll documentation site build and deployment in CI
|
||||
- Created `docs/index.html` to automatically redirect root path requests (`/`) to the English directory (`/en/`).
|
||||
- Updated `.github/workflows/docs-deploy.yml` to compile the Jekyll root site using `--baseurl /${{ github.event.repository.name }}` and setup Ruby with version `4.0.4` to match project release workflow conventions.
|
||||
- Updated `.github/workflows/docs-release.yml` to compile both versioned and root Jekyll sites, assemble them into `build/final_site/` with Dokka HTML references, configure correct baseurls respectively, and setup Ruby with version `4.0.4`.
|
||||
- Verified that local Gradle docs tasks (`generateDocsBundle`, `validateDocsBundle`, `publishDocsSite`) compile successfully and the redirect file is correctly populated in the output.
|
||||
|
||||
## 2026-05-20 — Optimized slow Flatpak CI jobs
|
||||
- Restrained flatpakGradleGenerator to target only the runtimeClasspath configuration in desktopApp and build-logic:convention modules.
|
||||
- Updated release.yml and reusable-check.yml to invoke only targeted tasks (:desktopApp:flatpakGradleGenerator and :build-logic:convention:flatpakGradleGenerator) instead of running the task on the root and all subprojects.
|
||||
- Retained Matrix architecture runner configuration for build validity, as Skiko/Compose Desktop native artifacts are resolved dynamically based on host architecture.
|
||||
- Cleaned up leftover speed-up workarounds: completely removed the Flatpak Gradle Generator plugin application and tasks from the root project and all other library/feature subprojects (`core:ble`, `core:common`, `core:database`, `core:model`, `core:navigation`, `core:proto`, and `feature:messaging`), including deleting the unused `flatpakKmpAndroidMeta` configuration from `feature:messaging`.
|
||||
- Verified that local execution of `:desktopApp:flatpakGradleGenerator` runtimeClasspath resolution speed dropped from 46 seconds to 12 seconds, and all Spotless and Detekt linting checks passed.
|
||||
|
||||
## 2026-05-12 — Implemented Apple alignment for docs feature (FR-038)
|
||||
- Branch: `feat/20260507-161858-app-docs-markdown`
|
||||
- Gap analysis against `meshtastic-apple` completed. Implemented 4 alignment items:
|
||||
1. Per-page TOC icons via `DocPageIconResolver.kt` mapping `iconId` to `MeshtasticIcons`
|
||||
2. New `docs/user/signal-meter.md` (RSSI vs SNR, bar-level criteria, LoRa signal concepts)
|
||||
3. New `docs/user/units-and-locale.md` (automatic metric/imperial via `MetricFormatter`)
|
||||
4. New `.github/workflows/docs-staleness.yml` (advisory PR comments for UI changes without doc updates)
|
||||
- Added `iconId: String?` field to `DocPage` and `KeywordIndexEntry` models
|
||||
- Updated `DocBundleLoader` with iconId for all 24 pages plus 2 new entries (signal-meter, units-and-locale)
|
||||
- Updated `DocsBrowserScreen` to show leading icons in TOC list items
|
||||
- Marked T061-T085 as completed in tasks.md (were implemented in prior session)
|
||||
- Added Phase 9 (T200-T206) for Apple alignment tasks — all marked complete
|
||||
- Skipped Apple-only features: watch, carplay, translate, TipKit, SwiftData docs
|
||||
- Verified: `spotlessApply`, `detekt`, `assembleDebug`, `compileKotlinJvm` — all green
|
||||
|
||||
## 2026-05-11 — Migrated feature/intro UI to commonMain
|
||||
- Moved intro onboarding UI composables and nav graph from `feature/intro/src/androidMain/` into `feature/intro/src/commonMain/`, adding shared `IntroPermissions` and `IntroSettingsNavigator` interfaces plus a common `introGraph` Navigation 3 extension.
|
||||
- Refactored `AppIntroductionScreen` into a thin Android host that provides Android permission/settings adapters via composition locals, and added `AndroidIntroPermissions`, `AndroidIntroSettingsNavigator`, and JVM desktop no-op stubs.
|
||||
|
||||
2
.github/agents/speckit.verify.run.agent.md
vendored
2
.github/agents/speckit.verify.run.agent.md
vendored
@@ -34,7 +34,7 @@ Validate the implementation against its specification artifacts (`spec.md`, `pla
|
||||
Run `.specify/scripts/bash/check-prerequisites.sh --json --paths-only` from repo root.
|
||||
|
||||
1. **Script succeeds** (on a feature branch): Parse JSON for FEATURE_DIR. Set `FEATURE_BRANCH = true`. Proceed to next step.
|
||||
2. **Script fails** (not on a feature branch): You MUST prompt for available features (Scan `specs/NNN-*/` to get available features). Use the **AskUserQuestion tool** to let the user select. **Do NOT guess or auto-select a change. Always let the user choose.**
|
||||
2. **Script fails** (not on a feature branch): You MUST prompt for available features (Scan `specs/*/` to get available features). Use the **AskUserQuestion tool** to let the user select. **Do NOT guess or auto-select a change. Always let the user choose.**
|
||||
|
||||
Derive absolute paths:
|
||||
|
||||
|
||||
4
.github/copilot-instructions.md
vendored
4
.github/copilot-instructions.md
vendored
@@ -18,7 +18,7 @@ git submodule update --init
|
||||
./gradlew :core:data:allTests
|
||||
|
||||
# Single module tests (Android-only module like :app)
|
||||
./gradlew :app:testFdroidDebugUnitTest
|
||||
./gradlew :androidApp:testFdroidDebugUnitTest
|
||||
|
||||
# Cross-platform compilation check (no tests)
|
||||
./gradlew kmpSmokeCompile
|
||||
@@ -46,7 +46,7 @@ KMP modules have different task names than pure-Android modules. Using the wrong
|
||||
|
||||
## Quick Reference
|
||||
|
||||
- **Architecture**: KMP project (Android, Desktop, iOS). Business logic in `commonMain`; platform shells (`app/`, `desktop/`) wire DI and host UI. See `AGENTS.md` and `.skills/kmp-architecture/`.
|
||||
- **Architecture**: KMP project (Android, Desktop, iOS). Business logic in `commonMain`; platform shells (`androidApp/`, `desktopApp/`) wire DI and host UI. See `AGENTS.md` and `.skills/kmp-architecture/`.
|
||||
- **Flavors**: `fdroid` (OSS) / `google` (Maps + DataDog). Only one installable at a time (different signing keys).
|
||||
- **Verify before push**: Run `./gradlew spotlessApply detekt assembleDebug test allTests`, then confirm CI with `gh pr checks <PR>`.
|
||||
- **Strings**: `stringResource(Res.string.key)` — run `python3 scripts/sort-strings.py` after adding strings.
|
||||
|
||||
@@ -5,7 +5,7 @@ excludeAgent: "code-review"
|
||||
|
||||
# CI Workflow Rules
|
||||
|
||||
- Prefer explicit Gradle task paths (`app:lintFdroidDebug`) over shorthand (`lintDebug`).
|
||||
- Prefer explicit Gradle task paths (`androidApp: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.
|
||||
|
||||
90
.github/workflows/docs-deploy.yml
vendored
Normal file
90
.github/workflows/docs-deploy.yml
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
name: Deploy Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
# Dokka sources (KDoc in source files)
|
||||
- 'androidApp/src/**'
|
||||
- 'core/**/src/**'
|
||||
- 'feature/**/src/**'
|
||||
- 'desktopApp/src/**'
|
||||
# Docs site sources
|
||||
- 'docs/**'
|
||||
- 'feature/docs/**'
|
||||
# Build infrastructure
|
||||
- 'build-logic/**'
|
||||
- 'build.gradle.kts'
|
||||
- 'settings.gradle.kts'
|
||||
- '.github/workflows/docs-deploy.yml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: true
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
with:
|
||||
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
|
||||
- name: Build Dokka HTML documentation
|
||||
run: ./gradlew dokkaGeneratePublicationHtml --no-configuration-cache
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '4.0.4'
|
||||
bundler-cache: true
|
||||
working-directory: docs
|
||||
|
||||
- name: Generate Docs Site
|
||||
run: ./gradlew generateDocsBundle validateDocsBundle publishDocsSite -Pdocs.channel=root -Pci=true
|
||||
|
||||
- name: Compile Jekyll Docs Site
|
||||
run: |
|
||||
BUNDLE_GEMFILE=docs/Gemfile bundle exec jekyll build \
|
||||
--source build/_site \
|
||||
--destination build/jekyll_site \
|
||||
--baseurl /${{ github.event.repository.name }}
|
||||
touch build/jekyll_site/.nojekyll
|
||||
|
||||
- name: Assemble Pages artifact
|
||||
run: |
|
||||
# Copy Dokka output into compiled jekyll_site/api/
|
||||
cp -r build/dokka/html build/jekyll_site/api
|
||||
|
||||
- name: Upload Pages Artifact
|
||||
uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: build/jekyll_site/
|
||||
|
||||
deploy:
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04-arm
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v5
|
||||
|
||||
403
.github/workflows/docs-governance.yml
vendored
Normal file
403
.github/workflows/docs-governance.yml
vendored
Normal file
@@ -0,0 +1,403 @@
|
||||
name: UI & Docs Governance
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize, reopened, labeled, unlabeled]
|
||||
schedule:
|
||||
- cron: "0 6 * * 1" # Every Monday at 6 AM UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
# ── Job 1: Staleness — flag PRs that change UI without updating docs ────────
|
||||
staleness:
|
||||
name: Docs staleness check
|
||||
runs-on: ubuntu-24.04-arm
|
||||
if: >-
|
||||
github.event_name == 'pull_request'
|
||||
&& !contains(github.event.pull_request.labels.*.name, 'skip-docs-check')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Detect changed files
|
||||
id: changed
|
||||
run: |
|
||||
BASE="${{ github.event.pull_request.base.sha }}"
|
||||
HEAD="${{ github.event.pull_request.head.sha }}"
|
||||
|
||||
changed=$(git diff --name-only "$BASE" "$HEAD")
|
||||
|
||||
views_changed=$(echo "$changed" | grep -E \
|
||||
'^feature/.*/src/commonMain/.*/(ui|component|screen)/|^feature/.*/src/androidMain/.*/ui/|^core/ui/src/commonMain/' \
|
||||
| grep -v 'Test\|Preview\|__Snapshots__' || true)
|
||||
|
||||
docs_changed=$(echo "$changed" | grep -E '^docs/en/(user|developer)/' || true)
|
||||
|
||||
echo "views_changed<<EOF" >> "$GITHUB_OUTPUT"
|
||||
echo "$views_changed" >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "docs_changed<<EOF" >> "$GITHUB_OUTPUT"
|
||||
echo "$docs_changed" >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
if [[ -n "$views_changed" && -z "$docs_changed" ]]; then
|
||||
echo "stale=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "stale=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Post warning comment
|
||||
if: steps.changed.outputs.stale == 'true'
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const viewsChanged = `${{ steps.changed.outputs.views_changed }}`.trim();
|
||||
const body = [
|
||||
'## 📄 Docs staleness check — advisory',
|
||||
'',
|
||||
'This PR modifies user-facing UI source files but does not update any page under `docs/en/user/` or `docs/en/developer/`.',
|
||||
'',
|
||||
'> ⚠️ Doc changes propagate to **3 consumers**: in-app docs browser, Jekyll site (GitHub Pages), and meshtastic.org (Docusaurus sync). Updating a page in `docs/en/` automatically flows to all three.',
|
||||
'',
|
||||
'**Changed source files:**',
|
||||
'```',
|
||||
viewsChanged,
|
||||
'```',
|
||||
'',
|
||||
'**What to check:**',
|
||||
'| Changed area | Likely doc page |',
|
||||
'|---|---|',
|
||||
'| `feature/messaging/` | `docs/en/user/messages-and-channels.md` |',
|
||||
'| `feature/node/` | `docs/en/user/nodes.md` or `docs/en/user/node-metrics.md` |',
|
||||
'| `feature/map/` | `docs/en/user/map-and-waypoints.md` |',
|
||||
'| `feature/connections/` | `docs/en/user/connections.md` |',
|
||||
'| `feature/settings/` | `docs/en/user/settings-radio-user.md` or `docs/en/user/settings-module-admin.md` |',
|
||||
'| `feature/firmware/` | `docs/en/user/firmware.md` |',
|
||||
'| `feature/intro/` | `docs/en/user/onboarding.md` |',
|
||||
'| `feature/discovery/` | `docs/en/user/discovery.md` |',
|
||||
'| `feature/docs/` | Internal docs infrastructure |',
|
||||
'| `core/ui/` | `docs/en/developer/codebase.md` or component-specific user pages |',
|
||||
'',
|
||||
'**New page checklist** (if adding a new doc page):',
|
||||
'1. Create the `.md` file in `docs/en/user/` or `docs/en/developer/` with `last_updated` frontmatter',
|
||||
'2. Register in `DocBundleLoader.kt` with string resources (in-app browser)',
|
||||
'3. Jekyll and Docusaurus sync pick up new pages automatically — no config change needed',
|
||||
'',
|
||||
'If this PR does **not** require a doc update (e.g., internal refactor, bug fix, test change), add the **`skip-docs-check`** label to dismiss this check.',
|
||||
'',
|
||||
'> **Cross-platform note:** This check is advisory while doc coverage matures. Both Android and Apple repos use the same `skip-docs-check` label and advisory severity. See `meshtastic/design` standards for shared conventions.',
|
||||
].join('\n');
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
const existing = comments.find(c =>
|
||||
c.user.login === 'github-actions[bot]' &&
|
||||
c.body.includes('Docs staleness check')
|
||||
);
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
- name: Dismiss stale comment when docs are updated
|
||||
if: steps.changed.outputs.stale == 'false'
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
const existing = comments.find(c =>
|
||||
c.user.login === 'github-actions[bot]' &&
|
||||
c.body.includes('Docs staleness check')
|
||||
);
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body: '## ✅ Docs staleness check passed\n\nThis PR includes updates to `docs/en/` alongside the source changes. Thank you!',
|
||||
});
|
||||
}
|
||||
|
||||
- name: Advisory status
|
||||
if: steps.changed.outputs.stale == 'true'
|
||||
run: |
|
||||
echo "::warning::UI source files changed without corresponding docs/en/ updates."
|
||||
echo "Add the 'skip-docs-check' label if this PR does not require a doc update."
|
||||
echo "NOTE: This check is advisory while docs coverage matures across platforms."
|
||||
echo "To upgrade to blocking, change this step to 'exit 1'."
|
||||
|
||||
# ── Job 2: Quality gates — link validation, coverage, registry, freshness ───
|
||||
validate:
|
||||
name: Docs quality gates
|
||||
runs-on: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- name: Validate internal links
|
||||
run: node scripts/validate-doc-links.js docs/en
|
||||
|
||||
- name: Check doc coverage
|
||||
run: node scripts/check-doc-coverage.js .
|
||||
|
||||
- name: Validate DocBundleLoader registry
|
||||
run: |
|
||||
loader="feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt"
|
||||
missing=0
|
||||
for f in docs/en/user/*.md docs/en/developer/*.md; do
|
||||
slug=$(basename "$f" .md)
|
||||
if ! grep -q "\"$slug\"" "$loader"; then
|
||||
echo "ERROR: $slug not registered in DocBundleLoader.kt"
|
||||
missing=$((missing + 1))
|
||||
fi
|
||||
done
|
||||
if [ "$missing" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "FAILED: $missing page(s) missing from DocBundleLoader.kt in-app index."
|
||||
exit 1
|
||||
fi
|
||||
echo "All doc pages registered in DocBundleLoader."
|
||||
|
||||
- name: Check doc freshness
|
||||
# Advisory — warns on stale pages but does not block PRs
|
||||
continue-on-error: true
|
||||
run: node scripts/check-doc-freshness.js docs --max-age-days=180
|
||||
|
||||
# ── Job 3: Preview staleness — flag UI changes without preview updates ──────
|
||||
preview-staleness:
|
||||
name: Preview staleness check
|
||||
runs-on: ubuntu-24.04-arm
|
||||
if: >-
|
||||
github.event_name == 'pull_request'
|
||||
&& !contains(github.event.pull_request.labels.*.name, 'skip-preview-check')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Detect changed files
|
||||
id: changed
|
||||
run: |
|
||||
BASE="${{ github.event.pull_request.base.sha }}"
|
||||
HEAD="${{ github.event.pull_request.head.sha }}"
|
||||
|
||||
changed=$(git diff --name-only "$BASE" "$HEAD")
|
||||
|
||||
# UI composables changed (screens, components — excluding tests and previews)
|
||||
ui_changed=$(echo "$changed" | grep -E \
|
||||
'^(feature|core/ui)/.*/src/commonMain/.*/.*\.(kt)$' \
|
||||
| grep -E '/(ui|component|screen)/' \
|
||||
| grep -v 'Test\|Preview\|__Snapshots__' || true)
|
||||
|
||||
# Preview files changed
|
||||
preview_changed=$(echo "$changed" | grep -E 'Preview.*\.kt$' || true)
|
||||
|
||||
# Screenshot test files changed
|
||||
screenshot_tests_changed=$(echo "$changed" | grep -E '^screenshot-tests/src/screenshotTest/.*\.kt$' || true)
|
||||
|
||||
# Reference images changed
|
||||
refs_changed=$(echo "$changed" | grep -E '^screenshot-tests/src/screenshotTestDebug/reference/.*\.png$' || true)
|
||||
|
||||
echo "ui_changed<<EOF" >> "$GITHUB_OUTPUT"
|
||||
echo "$ui_changed" >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "preview_changed<<EOF" >> "$GITHUB_OUTPUT"
|
||||
echo "$preview_changed" >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "screenshot_tests_changed<<EOF" >> "$GITHUB_OUTPUT"
|
||||
echo "$screenshot_tests_changed" >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "refs_changed<<EOF" >> "$GITHUB_OUTPUT"
|
||||
echo "$refs_changed" >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Preview staleness: UI changed but no preview updates
|
||||
if [[ -n "$ui_changed" && -z "$preview_changed" ]]; then
|
||||
echo "preview_stale=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "preview_stale=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Screenshot staleness: previews changed but no reference image updates
|
||||
if [[ -n "$preview_changed" && -z "$refs_changed" ]]; then
|
||||
echo "screenshot_stale=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "screenshot_stale=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Post preview advisory
|
||||
if: steps.changed.outputs.preview_stale == 'true'
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const uiChanged = `${{ steps.changed.outputs.ui_changed }}`.trim();
|
||||
const body = [
|
||||
'## 🖼️ Preview staleness check — advisory',
|
||||
'',
|
||||
'This PR modifies UI composables but does not update any `*Previews.kt` files.',
|
||||
'',
|
||||
'> Previews power screenshot tests and in-app docs screenshots. Keeping them current ensures visual regression coverage stays accurate.',
|
||||
'',
|
||||
'**Changed UI files:**',
|
||||
'```',
|
||||
uiChanged,
|
||||
'```',
|
||||
'',
|
||||
'**What to check:**',
|
||||
'| Pattern | Preview file convention |',
|
||||
'|---|---|',
|
||||
'| `feature/{name}/…/ui/` or `component/` | `feature/{name}/…/*Previews.kt` |',
|
||||
'| `core/ui/…/` | `core/ui/…/` (previews colocated) |',
|
||||
'',
|
||||
'**Adding previews checklist:**',
|
||||
'1. Create or update a `*Previews.kt` file in the same module with `@PreviewLightDark`',
|
||||
'2. Add `@Suppress("PreviewPublic")` if the preview is consumed by screenshot-tests',
|
||||
'3. Add corresponding `@PreviewTest` function in `screenshot-tests/src/screenshotTest/`',
|
||||
'4. Run `./gradlew :screenshot-tests:updateDebugScreenshotTest` to generate reference images',
|
||||
'',
|
||||
'If this PR does **not** require preview updates (e.g., logic-only change, non-visual refactor), add the **`skip-preview-check`** label to dismiss.',
|
||||
].join('\n');
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
const existing = comments.find(c =>
|
||||
c.user.login === 'github-actions[bot]' &&
|
||||
c.body.includes('Preview staleness check')
|
||||
);
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
- name: Post screenshot advisory
|
||||
if: steps.changed.outputs.screenshot_stale == 'true'
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const previewChanged = `${{ steps.changed.outputs.preview_changed }}`.trim();
|
||||
const body = [
|
||||
'## 📸 Screenshot reference staleness — advisory',
|
||||
'',
|
||||
'This PR modifies preview composables but does not update screenshot reference images.',
|
||||
'',
|
||||
'> Reference images in `screenshot-tests/src/screenshotTestDebug/reference/` must be regenerated when previews change, or `validateDebugScreenshotTest` will fail.',
|
||||
'',
|
||||
'**Changed preview files:**',
|
||||
'```',
|
||||
previewChanged,
|
||||
'```',
|
||||
'',
|
||||
'**How to update:**',
|
||||
'```bash',
|
||||
'./gradlew :screenshot-tests:updateDebugScreenshotTest',
|
||||
'```',
|
||||
'Then commit the updated reference PNGs.',
|
||||
'',
|
||||
'If this change is intentionally preview-only (e.g., adding a preview that doesn\'t need a test yet), add the **`skip-preview-check`** label.',
|
||||
].join('\n');
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
const existing = comments.find(c =>
|
||||
c.user.login === 'github-actions[bot]' &&
|
||||
c.body.includes('Screenshot reference staleness')
|
||||
);
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
- name: Dismiss comments when resolved
|
||||
if: >-
|
||||
steps.changed.outputs.preview_stale == 'false'
|
||||
&& steps.changed.outputs.screenshot_stale == 'false'
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
for (const marker of ['Preview staleness check', 'Screenshot reference staleness']) {
|
||||
const existing = comments.find(c =>
|
||||
c.user.login === 'github-actions[bot]' &&
|
||||
c.body.includes(marker)
|
||||
);
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body: `## ✅ ${marker} passed\n\nPreview and screenshot references are up to date.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
111
.github/workflows/docs-release.yml
vendored
Normal file
111
.github/workflows/docs-release.yml
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
name: Docs Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
- '!v*-*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: true
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Extract Version
|
||||
id: version
|
||||
run: |
|
||||
TAG=${GITHUB_REF#refs/tags/v}
|
||||
echo "version=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "Deploying docs for version: $TAG"
|
||||
|
||||
- name: Gradle Setup
|
||||
uses: ./.github/actions/gradle-setup
|
||||
with:
|
||||
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '4.0.4'
|
||||
bundler-cache: true
|
||||
working-directory: docs
|
||||
|
||||
# Build root docs site (/ on Pages)
|
||||
- name: Build Root Docs Site
|
||||
run: ./gradlew generateDocsBundle publishDocsSite -Pdocs.channel=root -Pci=true
|
||||
|
||||
# Build versioned docs (/vX.Y.Z/ on Pages)
|
||||
- name: Build Versioned Docs
|
||||
run: ./gradlew generateDocsBundle publishDocsSite -Pdocs.channel=release -Pdocs.version=${{ steps.version.outputs.version }} -Pci=true
|
||||
|
||||
# Build Dokka API reference (/api/ on Pages)
|
||||
- name: Build Dokka HTML documentation
|
||||
run: ./gradlew dokkaGeneratePublicationHtml --no-configuration-cache
|
||||
|
||||
- name: Compile Jekyll Sites
|
||||
run: |
|
||||
# Build versioned Jekyll site
|
||||
BUNDLE_GEMFILE=docs/Gemfile bundle exec jekyll build \
|
||||
--source build/_site/v${{ steps.version.outputs.version }} \
|
||||
--destination build/jekyll_release \
|
||||
--baseurl /${{ github.event.repository.name }}/v${{ steps.version.outputs.version }}
|
||||
|
||||
# Move versioned source folder out of root source folder to avoid nested build issues
|
||||
mv build/_site/v${{ steps.version.outputs.version }} build/v_temp
|
||||
|
||||
# Build root Jekyll site
|
||||
BUNDLE_GEMFILE=docs/Gemfile bundle exec jekyll build \
|
||||
--source build/_site \
|
||||
--destination build/jekyll_root \
|
||||
--baseurl /${{ github.event.repository.name }}
|
||||
|
||||
- name: Assemble Pages artifact
|
||||
run: |
|
||||
# Create final site directory
|
||||
mkdir -p build/final_site
|
||||
|
||||
# Copy root compiled site
|
||||
cp -r build/jekyll_root/* build/final_site/
|
||||
|
||||
# Copy release compiled site to vX.Y.Z/
|
||||
mkdir -p build/final_site/v${{ steps.version.outputs.version }}
|
||||
cp -r build/jekyll_release/* build/final_site/v${{ steps.version.outputs.version }}/
|
||||
|
||||
# Copy Dokka output into final_site/api/
|
||||
cp -r build/dokka/html build/final_site/api
|
||||
|
||||
# Disable GitHub Pages' own Jekyll build
|
||||
touch build/final_site/.nojekyll
|
||||
|
||||
- name: Upload Pages Artifact
|
||||
uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: build/final_site/
|
||||
|
||||
deploy:
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04-arm
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v5
|
||||
|
||||
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
|
||||
1
.github/workflows/main-check.yml
vendored
1
.github/workflows/main-check.yml
vendored
@@ -21,6 +21,5 @@ jobs:
|
||||
with:
|
||||
run_lint: true
|
||||
run_unit_tests: false
|
||||
run_desktop_builds: false
|
||||
upload_artifacts: true
|
||||
secrets: inherit
|
||||
|
||||
7
.github/workflows/pull-request.yml
vendored
7
.github/workflows/pull-request.yml
vendored
@@ -30,9 +30,9 @@ jobs:
|
||||
- '.github/workflows/**'
|
||||
- '.github/actions/**'
|
||||
# Product modules validated by reusable-check
|
||||
- 'app/**'
|
||||
- 'androidApp/**'
|
||||
- 'baselineprofile/**'
|
||||
- 'desktop/**'
|
||||
- 'desktopApp/**'
|
||||
- 'core/**'
|
||||
- 'feature/**'
|
||||
- 'screenshot-tests/**'
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
|
||||
# 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
|
||||
# (< 10 mins). Desktop compilation is already covered by the :desktopApp:test
|
||||
# task in the shard-app test shard.
|
||||
validate-and-build:
|
||||
needs: check-changes
|
||||
@@ -106,7 +106,6 @@ jobs:
|
||||
run_lint: true
|
||||
run_unit_tests: true
|
||||
run_coverage: false
|
||||
run_desktop_builds: false
|
||||
upload_artifacts: true
|
||||
secrets: inherit
|
||||
|
||||
|
||||
97
.github/workflows/release.yml
vendored
97
.github/workflows/release.yml
vendored
@@ -150,9 +150,9 @@ jobs:
|
||||
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
|
||||
rm -f ./androidApp/google-services.json
|
||||
echo $GSERVICES > ./androidApp/google-services.json
|
||||
echo $KEYSTORE | base64 -di > ./androidApp/$KEYSTORE_FILENAME
|
||||
echo "$KEYSTORE_PROPERTIES" > ./keystore.properties
|
||||
echo "datadogApplicationId=$DATADOG_APPLICATION_ID" >> ./secrets.properties
|
||||
echo "datadogClientToken=$DATADOG_CLIENT_TOKEN" >> ./secrets.properties
|
||||
@@ -172,14 +172,14 @@ jobs:
|
||||
run: bundle exec fastlane internal
|
||||
|
||||
- name: List outputs
|
||||
run: ls -R app/build/outputs/
|
||||
run: ls -R androidApp/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
|
||||
path: androidApp/build/outputs/bundle/googleRelease/androidApp-google-release.aab
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload Google APK artifact
|
||||
@@ -187,20 +187,20 @@ jobs:
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: google-apk
|
||||
path: app/build/outputs/apk/google/release/*.apk
|
||||
path: androidApp/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
|
||||
subject-path: androidApp/build/outputs/bundle/googleRelease/androidApp-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
|
||||
subject-path: androidApp/build/outputs/apk/google/release/*.apk
|
||||
|
||||
release-fdroid:
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -229,7 +229,7 @@ jobs:
|
||||
KEYSTORE_FILENAME: ${{ secrets.KEYSTORE_FILENAME }}
|
||||
KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }}
|
||||
run: |
|
||||
echo $KEYSTORE | base64 -di > ./app/$KEYSTORE_FILENAME
|
||||
echo $KEYSTORE | base64 -di > ./androidApp/$KEYSTORE_FILENAME
|
||||
echo "$KEYSTORE_PROPERTIES" > ./keystore.properties
|
||||
|
||||
- name: Setup Fastlane
|
||||
@@ -245,21 +245,21 @@ jobs:
|
||||
run: bundle exec fastlane fdroid_build
|
||||
|
||||
- name: List outputs
|
||||
run: ls -R app/build/outputs/
|
||||
run: ls -R androidApp/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
|
||||
path: androidApp/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
|
||||
subject-path: androidApp/build/outputs/apk/fdroid/release/*.apk
|
||||
|
||||
release-desktop:
|
||||
if: ${{ inputs.build_desktop }}
|
||||
@@ -307,13 +307,13 @@ jobs:
|
||||
# `-PaboutLibraries.release=true` as member access on `-PaboutLibraries`,
|
||||
# splitting the token and feeding `.release=true` to Gradle as a task name.
|
||||
run: >
|
||||
./gradlew :desktop:packageReleaseDistributionForCurrentOS
|
||||
${{ contains(runner.os, 'macOS') && ':desktop:packageReleaseUberJarForCurrentOS' || '' }}
|
||||
./gradlew :desktopApp:packageReleaseDistributionForCurrentOS
|
||||
${{ contains(runner.os, 'macOS') && ':desktopApp:packageReleaseUberJarForCurrentOS' || '' }}
|
||||
'-PaboutLibraries.release=true' --no-daemon
|
||||
|
||||
- name: List Desktop Binaries
|
||||
if: runner.os == 'Linux' || runner.os == 'macOS'
|
||||
run: ls -R desktop/build/compose/binaries/main-release
|
||||
run: ls -R desktopApp/build/compose/binaries/main-release
|
||||
|
||||
- name: Upload Desktop Artifacts
|
||||
if: always()
|
||||
@@ -321,13 +321,13 @@ jobs:
|
||||
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
|
||||
desktop/build/compose/jars/*-release.jar
|
||||
desktopApp/build/compose/binaries/main-release/*/*.dmg
|
||||
desktopApp/build/compose/binaries/main-release/*/*.msi
|
||||
desktopApp/build/compose/binaries/main-release/*/*.exe
|
||||
desktopApp/build/compose/binaries/main-release/*/*.deb
|
||||
desktopApp/build/compose/binaries/main-release/*/*.rpm
|
||||
desktopApp/build/compose/binaries/main-release/*/*.AppImage
|
||||
desktopApp/build/compose/jars/*-release.jar
|
||||
retention-days: 1
|
||||
if-no-files-found: ignore
|
||||
|
||||
@@ -336,13 +336,13 @@ jobs:
|
||||
uses: actions/attest-build-provenance@v4
|
||||
with:
|
||||
subject-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
|
||||
desktop/build/compose/jars/*-release.jar
|
||||
desktopApp/build/compose/binaries/main-release/*/*.dmg
|
||||
desktopApp/build/compose/binaries/main-release/*/*.msi
|
||||
desktopApp/build/compose/binaries/main-release/*/*.exe
|
||||
desktopApp/build/compose/binaries/main-release/*/*.deb
|
||||
desktopApp/build/compose/binaries/main-release/*/*.rpm
|
||||
desktopApp/build/compose/binaries/main-release/*/*.AppImage
|
||||
desktopApp/build/compose/jars/*-release.jar
|
||||
|
||||
create-flatpak-src:
|
||||
if: ${{ inputs.build_flatpak_src }}
|
||||
@@ -370,35 +370,20 @@ jobs:
|
||||
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
cache_read_only: 'true'
|
||||
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install ast-grep
|
||||
run: pip install ast-grep-cli
|
||||
|
||||
# Remove Android/iOS targets and other non-desktop bits
|
||||
# that would break the flatpakGradleGenerator
|
||||
- name: Prepare Offline Desktop Build
|
||||
run: ./scripts/desktop-only-prep.sh
|
||||
|
||||
- name: Generate Flatpak Sources
|
||||
env:
|
||||
DESKTOP_ONLY: true
|
||||
run: >
|
||||
./gradlew :build-logic:convention:flatpakGradleGenerator flatpakGradleGenerator
|
||||
--no-configuration-cache --refresh-dependencies --no-parallel
|
||||
./gradlew :combineFlatpakSources
|
||||
--no-configuration-cache
|
||||
|
||||
- name: List Flatpak source files
|
||||
run: ls -R flatpak-sources-*.json
|
||||
run: ls -R flatpak-sources.json
|
||||
|
||||
- name: Upload Flatpak source artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: flatpak-multisrc-${{ runner.arch }}
|
||||
path: flatpak-sources-*.json
|
||||
path: flatpak-sources.json
|
||||
retention-days: 1
|
||||
|
||||
release-flatpak-src:
|
||||
@@ -416,24 +401,12 @@ jobs:
|
||||
run: ls -R flatpak-multisrc/
|
||||
|
||||
- name: Validate Flatpak source files
|
||||
run: jq empty flatpak-multisrc/*/*.json
|
||||
run: jq empty flatpak-multisrc/*/flatpak-sources.json
|
||||
|
||||
- name: Combine Flatpak source files
|
||||
run: >
|
||||
jq -s 'add | unique_by(.dest + "/" + .["dest-filename"] + "/" + (.["only-arches"] | tostring))'
|
||||
flatpak-multisrc/*/*.json
|
||||
> flatpak-sources-combined.json
|
||||
|
||||
- name: Add mirror-urls for Maven Central
|
||||
run: >
|
||||
jq '[.[] | if (.url | test("^https://repo\\.maven\\.apache\\.org/maven2/")) then . + {"mirror-urls": [(.url | sub("^https://repo\\.maven\\.apache\\.org/maven2/"; "https://maven-central.storage-download.googleapis.com/maven2/"))]} else . end]'
|
||||
flatpak-sources-combined.json
|
||||
> flatpak-sources-central.json
|
||||
|
||||
- name: Add mirror-urls for Gradle Plugin Portal
|
||||
run: >
|
||||
jq '[.[] | if (.url | test("^https://plugins\\.gradle\\.org/m2/")) then . + {"mirror-urls": [(.url | sub("^https://plugins\\.gradle\\.org/m2/"; "https://maven.aliyun.com/repository/gradle-plugin/"))]} else . end]'
|
||||
flatpak-sources-central.json
|
||||
flatpak-multisrc/*/flatpak-sources.json
|
||||
> flatpak-sources.json
|
||||
|
||||
- name: Upload combined Flatpak source artifact
|
||||
|
||||
68
.github/workflows/reusable-check.yml
vendored
68
.github/workflows/reusable-check.yml
vendored
@@ -12,9 +12,6 @@ on:
|
||||
run_coverage:
|
||||
type: boolean
|
||||
default: true
|
||||
run_desktop_builds:
|
||||
type: boolean
|
||||
default: true
|
||||
run_desktop_flatpak_src:
|
||||
type: boolean
|
||||
default: false
|
||||
@@ -97,7 +94,7 @@ jobs:
|
||||
|
||||
- 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
|
||||
run: ./gradlew spotlessCheck detekt androidApp:lintFdroidDebug androidApp: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
|
||||
@@ -155,13 +152,13 @@ jobs:
|
||||
# See: https://github.com/meshtastic/Meshtastic-Android/issues/3231
|
||||
|
||||
echo "── Step 1: Verify aboutlibraries.json determinism ──"
|
||||
rm -f app/src/main/resources/aboutlibraries.json
|
||||
./gradlew :app:exportLibraryDefinitions -Pci=true --no-configuration-cache
|
||||
cp app/src/main/resources/aboutlibraries.json /tmp/aboutlibraries-run1.json
|
||||
rm -f androidApp/src/main/resources/aboutlibraries.json
|
||||
./gradlew :androidApp:exportLibraryDefinitions -Pci=true --no-configuration-cache
|
||||
cp androidApp/src/main/resources/aboutlibraries.json /tmp/aboutlibraries-run1.json
|
||||
|
||||
rm -f app/src/main/resources/aboutlibraries.json
|
||||
./gradlew :app:exportLibraryDefinitions -Pci=true --no-configuration-cache --rerun-tasks
|
||||
cp app/src/main/resources/aboutlibraries.json /tmp/aboutlibraries-run2.json
|
||||
rm -f androidApp/src/main/resources/aboutlibraries.json
|
||||
./gradlew :androidApp:exportLibraryDefinitions -Pci=true --no-configuration-cache --rerun-tasks
|
||||
cp androidApp/src/main/resources/aboutlibraries.json /tmp/aboutlibraries-run2.json
|
||||
|
||||
if ! diff -q /tmp/aboutlibraries-run1.json /tmp/aboutlibraries-run2.json; then
|
||||
echo "::error::aboutlibraries.json is NOT deterministic across runs!"
|
||||
@@ -171,9 +168,9 @@ jobs:
|
||||
echo "✅ aboutlibraries.json is deterministic"
|
||||
|
||||
echo "── Step 2: Build fdroid release APK ──"
|
||||
./gradlew :app:assembleFdroidRelease -Pci=true -Pmeshtastic.disableAbiSplits=true --no-configuration-cache
|
||||
./gradlew :androidApp:assembleFdroidRelease -Pci=true -Pmeshtastic.disableAbiSplits=true --no-configuration-cache
|
||||
|
||||
APK=$(find app/build/outputs/apk/fdroid/release -name "*.apk" | head -1)
|
||||
APK=$(find androidApp/build/outputs/apk/fdroid/release -name "*.apk" | head -1)
|
||||
if [ -z "$APK" ]; then
|
||||
echo "::error::No fdroid release APK found"
|
||||
exit 1
|
||||
@@ -298,7 +295,7 @@ jobs:
|
||||
# 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.)
|
||||
# shard-app: Pure-Android/JVM tests (androidApp, desktopApp, core:barcode, etc.)
|
||||
test-shards:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
@@ -362,17 +359,17 @@ jobs:
|
||||
:feature:settings:koverXmlReport
|
||||
- name: shard-app
|
||||
tasks: >-
|
||||
:app:testFdroidDebugUnitTest
|
||||
:app:testGoogleDebugUnitTest
|
||||
:desktop:test
|
||||
:androidApp:testFdroidDebugUnitTest
|
||||
:androidApp:testGoogleDebugUnitTest
|
||||
:desktopApp:test
|
||||
:core:barcode:testFdroidDebugUnitTest
|
||||
:core:barcode:testGoogleDebugUnitTest
|
||||
kover: >-
|
||||
:app:koverXmlReportFdroidDebug
|
||||
:app:koverXmlReportGoogleDebug
|
||||
:androidApp:koverXmlReportFdroidDebug
|
||||
:androidApp:koverXmlReportGoogleDebug
|
||||
:core:barcode:koverXmlReportFdroidDebug
|
||||
:core:barcode:koverXmlReportGoogleDebug
|
||||
:desktop:koverXmlReport
|
||||
:desktopApp:koverXmlReport
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -450,14 +447,14 @@ jobs:
|
||||
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
|
||||
run: ./gradlew androidApp:assembleFdroidDebug androidApp: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
|
||||
path: androidApp/build/outputs/apk/*/debug/*.apk
|
||||
retention-days: 7
|
||||
|
||||
- name: Report App Size
|
||||
@@ -466,7 +463,7 @@ jobs:
|
||||
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
|
||||
find androidApp/build/outputs/apk -name "*.apk" -exec du -h {} + | awk '{print "| " $2 " | " $1 " |"}' >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ── Desktop Build ───────────────────────────────────────────────────
|
||||
build-desktop:
|
||||
@@ -498,14 +495,14 @@ jobs:
|
||||
cache_read_only: ${{ needs.lint-check.outputs.cache_read_only }}
|
||||
|
||||
- name: Build Desktop
|
||||
run: ./gradlew :desktop:createDistributable -Pci=true --scan
|
||||
run: ./gradlew :desktopApp: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/
|
||||
path: desktopApp/build/compose/binaries/main/app/
|
||||
retention-days: 7
|
||||
|
||||
# ── Flatpak Sources ───────────────────────────────────────────────────
|
||||
@@ -536,33 +533,18 @@ jobs:
|
||||
with:
|
||||
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
cache_read_only: true
|
||||
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install ast-grep
|
||||
run: pip install ast-grep-cli
|
||||
|
||||
# Remove Android/iOS targets and other non-desktop bits
|
||||
# that would break the flatpakGradleGenerator
|
||||
- name: Prepare Offline Desktop Build
|
||||
run: ./scripts/desktop-only-prep.sh
|
||||
|
||||
- name: Generate Flatpak Sources
|
||||
env:
|
||||
DESKTOP_ONLY: true
|
||||
run: >
|
||||
./gradlew :build-logic:convention:flatpakGradleGenerator flatpakGradleGenerator
|
||||
--no-configuration-cache --refresh-dependencies --no-parallel
|
||||
./gradlew :combineFlatpakSources
|
||||
--no-configuration-cache --refresh-dependencies
|
||||
|
||||
- run: ls -lah flatpak-sources-*.json
|
||||
- run: ls -lah flatpak-sources.json
|
||||
|
||||
- name: Upload Flatpak Sources
|
||||
if: ${{ inputs.upload_artifacts }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: flatpak-sources-${{ runner.arch }}
|
||||
path: flatpak-sources-*.json
|
||||
path: flatpak-sources.json
|
||||
retention-days: 7
|
||||
|
||||
9
.github/workflows/scheduled-updates.yml
vendored
9
.github/workflows/scheduled-updates.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Update firmware releases list
|
||||
id: firmware
|
||||
run: |
|
||||
firmware_file_path="app/src/main/assets/firmware_releases.json"
|
||||
firmware_file_path="androidApp/src/main/assets/firmware_releases.json"
|
||||
temp_firmware_file="/tmp/new_firmware_releases.json"
|
||||
|
||||
echo "Fetching latest firmware releases..."
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
- name: Update hardware list
|
||||
id: hardware
|
||||
run: |
|
||||
hardware_file_path="app/src/main/assets/device_hardware.json"
|
||||
hardware_file_path="androidApp/src/main/assets/device_hardware.json"
|
||||
temp_hardware_file="/tmp/new_device_hardware.json"
|
||||
|
||||
echo "Fetching latest device hardware data..."
|
||||
@@ -172,11 +172,12 @@ jobs:
|
||||
base: 'main'
|
||||
delete-branch: true
|
||||
add-paths: |
|
||||
app/src/main/assets/firmware_releases.json
|
||||
app/src/main/assets/device_hardware.json
|
||||
androidApp/src/main/assets/firmware_releases.json
|
||||
androidApp/src/main/assets/device_hardware.json
|
||||
fastlane/metadata/android/**
|
||||
**/strings.xml
|
||||
**/README.md
|
||||
docs/**/*.md
|
||||
labels: |
|
||||
automation
|
||||
l10n
|
||||
|
||||
53
.github/workflows/sync-android-docs.yml
vendored
Normal file
53
.github/workflows/sync-android-docs.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Sync Android App Documentation
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * 0" # Every Sunday at midnight UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout meshtastic/meshtastic
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Clone meshtastic/Meshtastic-Android
|
||||
run: |
|
||||
git clone --depth=1 --branch main https://github.com/meshtastic/Meshtastic-Android.git /tmp/Meshtastic-Android
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Install cwebp
|
||||
run: sudo apt-get update && sudo apt-get install -y webp
|
||||
|
||||
- name: Run sync script
|
||||
run: node /tmp/Meshtastic-Android/scripts/sync-android-docs.js /tmp/Meshtastic-Android --convert-webp
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "docs: sync latest Android app documentation"
|
||||
branch: sync/android-docs
|
||||
delete-branch: true
|
||||
title: "docs: sync latest Android app documentation"
|
||||
body: |
|
||||
This PR was automatically created by the [sync-android-docs](../.github/workflows/sync-android-docs.yml) workflow.
|
||||
|
||||
It synchronizes Markdown documentation and images from [meshtastic/Meshtastic-Android](https://github.com/meshtastic/Meshtastic-Android) into this repository:
|
||||
|
||||
- Markdown files → `docs/software/android/`
|
||||
- Image files → `static/img/android/docs/`
|
||||
|
||||
Image paths in Markdown are rewritten to use the Docusaurus `/img/android/docs/` static path.
|
||||
Screenshots are converted to WebP for optimal site performance.
|
||||
8
.github/workflows/update-changelog.yml
vendored
8
.github/workflows/update-changelog.yml
vendored
@@ -101,10 +101,14 @@ jobs:
|
||||
| sed -e :a -e '/^\n*$/{$d;N;ba}'
|
||||
}
|
||||
|
||||
# Helper: generate notes using git log (works between any two refs)
|
||||
# Helper: generate notes using GitHub compare API (works between any two refs)
|
||||
generate_notes_git() {
|
||||
local from_ref="$1" to_ref="$2"
|
||||
git log --no-merges --format="* %s by @%an in [\`%h\`](https://github.com/${REPO}/commit/%H)" "$from_ref".."$to_ref" 2>/dev/null || true
|
||||
gh api "repos/${REPO}/compare/${from_ref}...${to_ref}" \
|
||||
--jq '.commits[]
|
||||
| select(.parents | length == 1)
|
||||
| "* \(.commit.message | split("\n")[0]) by \(.commit.author.name) (@\(.author.login // .commit.author.name)) in [`\(.sha[0:9])`](https://github.com/'"${REPO}"'/commit/\(.sha))"' \
|
||||
2>/dev/null || true
|
||||
}
|
||||
|
||||
# Helper: demote ### headings to #### for nesting under channel sections
|
||||
|
||||
20
.gitignore
vendored
20
.gitignore
vendored
@@ -51,6 +51,12 @@ docs/screenshots/
|
||||
build-and-install-android.sh
|
||||
wireless-install.sh
|
||||
|
||||
# Generated docs artifacts
|
||||
docs/_site/
|
||||
docs/.jekyll-cache/
|
||||
docs/.jekyll-metadata
|
||||
docs/Gemfile.lock
|
||||
|
||||
# Git worktrees
|
||||
.worktrees/
|
||||
/firebase-debug.log.jdk/
|
||||
@@ -63,6 +69,18 @@ firebase-debug.log
|
||||
/coil/
|
||||
/kable/
|
||||
.opencode/
|
||||
# Synced docs in composeResources (generated from docs/ source by syncDocsToComposeResources task)
|
||||
feature/docs/src/commonMain/composeResources/files/docs/
|
||||
# Synced translated docs (generated from docs/{locale}/ by syncTranslatedDocsToComposeResources task)
|
||||
feature/docs/src/commonMain/composeResources/files/*/docs/
|
||||
|
||||
/desktop/bin/
|
||||
/build-logic/convention/bin/
|
||||
/.specify/extensions/.cache/
|
||||
/.specify/extensions/.cache/
|
||||
# Jekyll local config (comments out remote_theme for local builds)
|
||||
docs/_config_local.yml
|
||||
|
||||
# Flatpak source manifests and repositories
|
||||
flatpak-sources-*.json
|
||||
flatpak-sources.json
|
||||
offline-repository/
|
||||
|
||||
1
.gitmodules
vendored
1
.gitmodules
vendored
@@ -1,3 +1,4 @@
|
||||
[submodule "app proto submodule"]
|
||||
path = core/proto/src/main/proto
|
||||
url = https://github.com/meshtastic/protobufs.git
|
||||
branch = master
|
||||
|
||||
@@ -23,8 +23,8 @@ When reviewing code, meticulously verify the following categories. Flag any devi
|
||||
- [ ] **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).
|
||||
- [ ] **Placeholders:** Require `PlaceholderScreen(name)` from `core:ui/commonMain` for unimplemented desktopApp/JVM features. No inline placeholders in feature modules.
|
||||
- [ ] **Adaptive Layouts:** Verify use of `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` to support desktopApp/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.
|
||||
@@ -56,8 +56,8 @@ When reviewing code, meticulously verify the following categories. Flag any devi
|
||||
- [ ] **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.
|
||||
- [ ] **New Dependencies:** If a new reflection-heavy dependency is added (DI, serialization, JNI, ServiceLoader), verify keep rules exist in **both** `androidApp/proguard-rules.pro` (R8) and `desktopApp/proguard-rules.pro` (ProGuard). The two files must stay aligned.
|
||||
- [ ] **Release Smoke-Test:** For dependency or ProGuard rule changes, verify `assembleRelease` and `./gradlew :desktopApp:runRelease` succeed.
|
||||
|
||||
## Review Output Guidelines
|
||||
1. **Be Specific & Constructive:** Provide exact file references and code snippets illustrating the required project pattern.
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
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.
|
||||
- **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 desktopApp/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.
|
||||
- **Placeholders:** Use `PlaceholderScreen(name)` from `core:ui/commonMain` for unimplemented desktopApp/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`.
|
||||
|
||||
@@ -58,4 +58,4 @@ Choose the right tool for the job:
|
||||
## 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`
|
||||
- **Provider wiring:** `androidApp/src/main/kotlin/org/meshtastic/app/MainActivity.kt`
|
||||
|
||||
53
.skills/compose-ui/strings-index.txt
generated
53
.skills/compose-ui/strings-index.txt
generated
@@ -302,6 +302,47 @@ distance_filters_description
|
||||
distance_measurements
|
||||
distance_measurements_description
|
||||
dns
|
||||
### DOC ###
|
||||
doc_clear_search
|
||||
doc_keywords_connections
|
||||
doc_keywords_desktop
|
||||
doc_keywords_discovery
|
||||
doc_keywords_firmware
|
||||
doc_keywords_map
|
||||
doc_keywords_measurement
|
||||
doc_keywords_messages
|
||||
doc_keywords_mqtt
|
||||
doc_keywords_node_metrics
|
||||
doc_keywords_nodes
|
||||
doc_keywords_onboarding
|
||||
doc_keywords_settings_module
|
||||
doc_keywords_settings_radio
|
||||
doc_keywords_signal_meter
|
||||
doc_keywords_tak
|
||||
doc_keywords_telemetry
|
||||
doc_keywords_translate
|
||||
doc_keywords_units
|
||||
doc_search_placeholder
|
||||
doc_section_developer
|
||||
doc_section_user
|
||||
doc_title_connections
|
||||
doc_title_desktop
|
||||
doc_title_discovery
|
||||
doc_title_firmware
|
||||
doc_title_map
|
||||
doc_title_measurement
|
||||
doc_title_messages
|
||||
doc_title_mqtt
|
||||
doc_title_node_metrics
|
||||
doc_title_nodes
|
||||
doc_title_onboarding
|
||||
doc_title_settings_module
|
||||
doc_title_settings_radio
|
||||
doc_title_signal_meter
|
||||
doc_title_tak
|
||||
doc_title_telemetry
|
||||
doc_title_translate
|
||||
doc_title_units
|
||||
done
|
||||
dont_show_again_for_device
|
||||
double_tap_as_button_press
|
||||
@@ -483,6 +524,7 @@ hardware
|
||||
hardware_model
|
||||
heading
|
||||
heartbeat
|
||||
help_and_documentation
|
||||
hide_layer
|
||||
hide_password
|
||||
history_return_max
|
||||
@@ -1143,8 +1185,19 @@ tak_role_sniper
|
||||
tak_role_teamlead
|
||||
tak_role_teammember
|
||||
tak_role_unspecified
|
||||
tak_server
|
||||
tak_server_enabled
|
||||
tak_server_enabled_desc
|
||||
tak_server_export_data_package_desc
|
||||
tak_server_loading
|
||||
tak_server_section
|
||||
tak_server_test_card_title
|
||||
tak_server_test_idle
|
||||
tak_server_test_result_bytes
|
||||
tak_server_test_result_unknown_error
|
||||
tak_server_test_results
|
||||
tak_server_test_run
|
||||
tak_server_test_running
|
||||
tak_team
|
||||
tak_team_blue
|
||||
tak_team_brown
|
||||
|
||||
@@ -35,7 +35,7 @@ A step-by-step workflow for implementing a new feature in the Meshtastic-Android
|
||||
```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:
|
||||
- If the feature adds a new reflection-heavy dependency, add keep rules to **both** `androidApp/proguard-rules.pro` and `desktopApp/proguard-rules.pro`, then verify release builds:
|
||||
```bash
|
||||
./gradlew assembleFdroidRelease :desktop:runRelease
|
||||
./gradlew assembleFdroidRelease :desktopApp:runRelease
|
||||
```
|
||||
|
||||
@@ -52,10 +52,10 @@ Guidelines on managing Kotlin Multiplatform (KMP) source-sets, expected abstract
|
||||
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.
|
||||
4. For desktop wiring, copy the pattern in `desktopApp/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`
|
||||
- **Desktop DI Stubs:** `desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt`
|
||||
- **Version Catalog:** `gradle/libs.versions.toml`
|
||||
- **Convention Plugins:** `build-logic/convention/`
|
||||
|
||||
@@ -7,7 +7,7 @@ This skill covers dependency injection (Koin Annotations 4.2.x) and JetBrains Na
|
||||
|
||||
### 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`.
|
||||
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 `androidApp/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt` and `desktopApp/.../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.
|
||||
|
||||
@@ -49,8 +49,8 @@ startKoin<AndroidKoinApp> {
|
||||
- **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`
|
||||
- **App Startup / Koin Bootstrap:** `androidApp/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt`
|
||||
- **DI Bootstrap Object:** `androidApp/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt`
|
||||
- **DI App Wiring:** `androidApp/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`
|
||||
- **Desktop Nav Shell:** `desktopApp/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt`
|
||||
|
||||
@@ -11,7 +11,7 @@ Module directory, namespacing conventions, environment setup, and troubleshootin
|
||||
|
||||
| Directory | Description |
|
||||
| :--- | :--- |
|
||||
| `app/` | Main application module. Contains `MainActivity`, Koin DI modules, and app-level logic. Uses package `org.meshtastic.app`. |
|
||||
| `androidApp/` | 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. |
|
||||
@@ -38,7 +38,7 @@ Module directory, namespacing conventions, environment setup, and troubleshootin
|
||||
| `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`. |
|
||||
| `desktopApp/` | 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.
|
||||
|
||||
@@ -52,7 +52,7 @@ The standard SDD cycle for a new feature:
|
||||
|
||||
```text
|
||||
1. /speckit.specify "Feature description here"
|
||||
→ Creates specs/<NNN>-feature-name/spec.md
|
||||
→ Creates specs/<YYYYMMDD-HHMMSS>-feature-name/spec.md
|
||||
→ Auto-creates feature branch via git hook
|
||||
|
||||
2. /speckit.clarify
|
||||
@@ -85,11 +85,11 @@ This runs: specify → (review gate) → plan → (review gate) → tasks → im
|
||||
|
||||
## File Structure
|
||||
|
||||
Spec Kit produces files under `specs/<NNN>-feature-name/`:
|
||||
Spec Kit produces files under `specs/<YYYYMMDD-HHMMSS>-feature-name/`:
|
||||
|
||||
```
|
||||
specs/
|
||||
└── 001-feature-name/
|
||||
└── 20260513-160000-feature-name/
|
||||
├── spec.md # Feature specification (FRs, NFRs, SCs, user stories)
|
||||
├── plan.md # Implementation plan (architecture, phases)
|
||||
├── tasks.md # Dependency-ordered task list
|
||||
|
||||
@@ -17,10 +17,32 @@ Run in a single invocation for routine changes to ensure code formatting, analys
|
||||
> 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.
|
||||
> Conversely, `allTests` does **not** cover pure-Android modules (`:androidApp`, `: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.*
|
||||
|
||||
### SharedFlow + backgroundScope in `runTest`
|
||||
|
||||
When testing long-lived coroutines (e.g., `Flow.collect` loops launched in `backgroundScope`), **use `runTest(UnconfinedTestDispatcher())`** instead of plain `runTest`:
|
||||
|
||||
```kotlin
|
||||
// ❌ BAD — SharedFlow emissions silently never reach collectors
|
||||
@Test fun `inbound packet is forwarded`() = runTest {
|
||||
backgroundScope.launch { sut.start(backgroundScope) }
|
||||
sharedFlow.emit(packet)
|
||||
// assertion fails — collector never receives the emission
|
||||
}
|
||||
|
||||
// ✅ GOOD — UnconfinedTestDispatcher eagerly dispatches subscriber resumptions
|
||||
@Test fun `inbound packet is forwarded`() = runTest(UnconfinedTestDispatcher()) {
|
||||
backgroundScope.launch { sut.start(backgroundScope) }
|
||||
sharedFlow.emit(packet)
|
||||
// assertion passes — collector receives emission immediately
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** `backgroundScope` uses `StandardTestDispatcher` by default, which does **not** eagerly dispatch `SharedFlow` subscriber resumptions. Even `advanceUntilIdle()` won't trigger delivery. `UnconfinedTestDispatcher()` fixes this by dispatching eagerly. This affects any test where a coroutine in `backgroundScope` collects from a `SharedFlow` or `MutableSharedFlow`.
|
||||
|
||||
## 2) Change-type verification matrix
|
||||
|
||||
- `docs-only` changes: Usually no Gradle run required, but run `spotlessCheck` if practical.
|
||||
|
||||
@@ -30,7 +30,7 @@ Validate the implementation against its specification artifacts (`spec.md`, `pla
|
||||
Run `{SCRIPT}` from repo root.
|
||||
|
||||
1. **Script succeeds** (on a feature branch): Parse JSON for FEATURE_DIR. Set `FEATURE_BRANCH = true`. Proceed to next step.
|
||||
2. **Script fails** (not on a feature branch): You MUST prompt for available features (Scan `specs/NNN-*/` to get available features). Use the **AskUserQuestion tool** to let the user select. **Do NOT guess or auto-select a change. Always let the user choose.**
|
||||
2. **Script fails** (not on a feature branch): You MUST prompt for available features (Scan `specs/*/` to get available features). Use the **AskUserQuestion tool** to let the user select. **Do NOT guess or auto-select a change. Always let the user choose.**
|
||||
|
||||
Derive absolute paths:
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"ai": "opencode",
|
||||
"branch_numbering": "sequential",
|
||||
"branch_numbering": "timestamp",
|
||||
"context_file": "AGENTS.md",
|
||||
"here": true,
|
||||
"integration": "opencode",
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
<!--
|
||||
SYNC IMPACT REPORT
|
||||
==================
|
||||
Version change: 1.1.1 → 1.2.0
|
||||
Version change: 1.2.0 → 1.3.0
|
||||
Modified principles:
|
||||
- V. Design Standards Compliance (expanded to require cross-platform behavior spec
|
||||
from meshtastic/design/features/ for multi-platform features)
|
||||
Added sections: None.
|
||||
- VI. Verify Before Push → renumbered to VII.
|
||||
Added sections:
|
||||
- VI. Documentation Freshness (new principle requiring last_updated frontmatter,
|
||||
blocking staleness check, link validation, coverage checks, freshness warnings)
|
||||
Removed sections: None.
|
||||
Templates requiring updates:
|
||||
✅ .specify/templates/spec-template.md — Added Cross-Platform Spec metadata field and
|
||||
cross-platform check guidance in Summary comment.
|
||||
✅ .specify/templates/plan-template.md — Constitution Check V updated to require upstream
|
||||
spec link for cross-platform features.
|
||||
✅ .specify/templates/checklist-template.md — CHK005 updated to include cross-platform spec check.
|
||||
Follow-up TODOs: None.
|
||||
- .specify/templates/checklist-template.md — Add CHK006 for documentation freshness
|
||||
- .specify/templates/plan-template.md — Constitution Check VII for docs freshness
|
||||
Follow-up TODOs: Update AGENTS.md with docs governance reference.
|
||||
-->
|
||||
|
||||
# Meshtastic Android (KMP) Constitution
|
||||
@@ -90,7 +88,41 @@ All user-facing UI MUST conform to the Meshtastic Client Design Standards:
|
||||
users have a predictable experience regardless of platform. The design standards are
|
||||
maintained collaboratively across all Meshtastic client teams.
|
||||
|
||||
### VI. Verify Before Push
|
||||
### VI. Documentation Freshness
|
||||
|
||||
In-app documentation MUST remain accurate and current as the codebase evolves.
|
||||
Documentation changes propagate to **three consumers** — all three MUST be considered:
|
||||
|
||||
1. **In-app docs browser** — `syncDocsToComposeResources` copies `docs/` into Compose
|
||||
Resources at build time. Changes are bundled into the app automatically.
|
||||
2. **Jekyll site** (GitHub Pages) — `docs/` is served directly. The `docs-deploy.yml`
|
||||
workflow rebuilds on push to `main`.
|
||||
3. **Docusaurus site** (meshtastic.org) — `scripts/sync-android-docs.js` transforms
|
||||
`docs/` for the external site. Runs weekly via the `meshtastic/meshtastic` repo.
|
||||
|
||||
Governance rules:
|
||||
|
||||
- Every doc page MUST include a `last_updated` frontmatter field (YYYY-MM-DD).
|
||||
Update this field whenever page content changes.
|
||||
- PRs that modify user-facing UI source files MUST update the corresponding doc page(s)
|
||||
or apply the `skip-docs-check` label with justification. The docs staleness check is a
|
||||
**blocking** CI gate.
|
||||
- Internal cross-references between doc pages and image paths MUST be validated; broken
|
||||
links fail the `docs-governance` workflow.
|
||||
- Every user-facing feature module MUST have corresponding documentation in `docs/user/`
|
||||
or `docs/developer/`. Coverage is checked by `scripts/check-doc-coverage.js`.
|
||||
- Pages older than 180 days without updates trigger an advisory freshness warning.
|
||||
- New doc pages MUST be registered in `DocBundleLoader.kt` (in-app index), and added to
|
||||
the `KNOWN_*_SLUGS` sets in `sync-android-docs.js` (Docusaurus link resolution).
|
||||
Jekyll picks up new pages automatically via `_config.yml` scope-based defaults.
|
||||
- Image references MUST use root-relative paths (`/assets/screenshots/filename.png`) so
|
||||
they resolve correctly in both Jekyll and the in-app renderer. The sync script rewrites
|
||||
these to Docusaurus paths automatically.
|
||||
- Rationale: Documentation that drifts from the implementation misleads users, increases
|
||||
support burden, and undermines the in-app help experience. Three distinct consumers
|
||||
means a single source change must be verified across all delivery channels.
|
||||
|
||||
### VII. Verify Before Push
|
||||
|
||||
Local verification MUST complete successfully before any `git push`:
|
||||
|
||||
@@ -162,7 +194,7 @@ This constitution supersedes all other practices, coding guidelines, and agent i
|
||||
- PATCH: Clarifications, wording fixes, or non-semantic refinements.
|
||||
|
||||
**Compliance Review**: Every implementation plan and PR description MUST include a
|
||||
Constitution Check confirming all six principles were evaluated. Complexity violations
|
||||
Constitution Check confirming all seven principles were evaluated. Complexity violations
|
||||
require explicit justification in the Complexity Tracking table of the plan document.
|
||||
|
||||
**Version**: 1.2.0 | **Ratified**: 2026-05-07 | **Last Amended**: 2026-05-12
|
||||
**Version**: 1.3.0 | **Ratified**: 2026-05-07 | **Last Amended**: 2026-05-13
|
||||
|
||||
133
CHANGELOG.md
133
CHANGELOG.md
@@ -9,71 +9,71 @@ See [GitHub Releases](https://github.com/meshtastic/Meshtastic-Android/releases)
|
||||
|
||||
### Unreleased (not yet in any build)
|
||||
|
||||
* refactor: use immutable destNum in RadioConfigViewModel (#5436) by @James Rich in [`1976808a3`](https://github.com/meshtastic/Meshtastic-Android/commit/1976808a361942f2c358329807d3605b8898e36d)
|
||||
* docs: update CHANGELOG.md (#5431) by @github-actions[bot] in [`57b0200b6`](https://github.com/meshtastic/Meshtastic-Android/commit/57b0200b6123e87f5037b12128411b69eee2ad23)
|
||||
* Flatpak: Add Maven/Gradle mirror URLs (#5433) by @Austin in [`ada16f4f5`](https://github.com/meshtastic/Meshtastic-Android/commit/ada16f4f5f2e859624ad8588ff07cdbff945aac0)
|
||||
* fix: update screenshots (#5435) by @James Rich in [`cf0a7cdab`](https://github.com/meshtastic/Meshtastic-Android/commit/cf0a7cdabff85e97accf4522709dfcb0fd5d4298)
|
||||
* chore: clean up brownfield specs and migrate to timestamp naming (#5432) by @James Rich in [`73469b415`](https://github.com/meshtastic/Meshtastic-Android/commit/73469b415e742a6601790b9574e1cf8a4a43a52c)
|
||||
* revert: Update retry settings in gradle-wrapper.properties (#5430) by @James Rich in [`ff9d6881c`](https://github.com/meshtastic/Meshtastic-Android/commit/ff9d6881c01b8e9ec7654179bfa559e93bea9974)
|
||||
* build(flatpak): overhaul and automate flatpak offline manifest generation (#5529) by James Rich (@jamesarich) in [`99b5688b0`](https://github.com/meshtastic/Meshtastic-Android/commit/99b5688b00e5810467769ae57e6ee33ea3d3360c)
|
||||
|
||||
### Internal (v2.7.14-internal.90)
|
||||
Changes since [`v2.7.14-open.8`](https://github.com/meshtastic/Meshtastic-Android/releases/tag/v2.7.14-open.8):
|
||||
### Internal (v2.7.14-internal.95)
|
||||
Changes since [`v2.7.14-open.9`](https://github.com/meshtastic/Meshtastic-Android/releases/tag/v2.7.14-open.9):
|
||||
|
||||
* docs: update CHANGELOG.md (#5427) by @github-actions[bot] in [`1b902ba07`](https://github.com/meshtastic/Meshtastic-Android/commit/1b902ba07c7a6f5b41bba9f4cb439e7de0925e4c)
|
||||
* chore(deps): update gradle to v9.5.1 (#5429) by @renovate[bot] in [`010ff358d`](https://github.com/meshtastic/Meshtastic-Android/commit/010ff358d0cf5c586b557af186ffdd1ef719be81)
|
||||
* fix: clarify position precision as ± radius (#5428) by @James Rich in [`dcb147163`](https://github.com/meshtastic/Meshtastic-Android/commit/dcb147163c3a5c8dada885ee6e84790fcc24f651)
|
||||
* fix(desktop): keep Vico package to prevent bytecode verification errors (#5424) by @James Rich in [`ab4589c06`](https://github.com/meshtastic/Meshtastic-Android/commit/ab4589c06a849743268f926bbe52723c833add3d)
|
||||
* build: conditionalize JitPack repository for desktop-only builds (#5426) by @James Rich in [`fcf0b5074`](https://github.com/meshtastic/Meshtastic-Android/commit/fcf0b507461796ff2e84265de728283ed5537073)
|
||||
* docs: update CHANGELOG.md (#5422) by @github-actions[bot] in [`3bfc66eb2`](https://github.com/meshtastic/Meshtastic-Android/commit/3bfc66eb29c438c3f2d68a9225eba9fb3d2c2e25)
|
||||
* fix: enhance uniqueness check for Flatpak source files in release workflow. (#5423) by @Austin in [`a2dd4d18a`](https://github.com/meshtastic/Meshtastic-Android/commit/a2dd4d18aab2ed1a9000e4ac3ed53260658a4734)
|
||||
* chore(deps): update core/proto/src/main/proto digest to 1c62540 (#5421) by @renovate[bot] in [`05b1ea918`](https://github.com/meshtastic/Meshtastic-Android/commit/05b1ea918ef3f14b48f1777402e645df900dd337)
|
||||
* docs: update CHANGELOG.md (#5420) by @github-actions[bot] in [`827535ad4`](https://github.com/meshtastic/Meshtastic-Android/commit/827535ad45778e1eb5b369be70c955b300678a37)
|
||||
* fix: clamp future lastHeard timestamps to current time on ingestion (#5418) by @James Rich in [`0f2b1c064`](https://github.com/meshtastic/Meshtastic-Android/commit/0f2b1c064aa9b77685ac8a7d05fe81af89655027)
|
||||
* fix(ci): resolve flatpak source merge race condition (#5419) by @James Rich in [`6f42f0788`](https://github.com/meshtastic/Meshtastic-Android/commit/6f42f078878258b334ae5b234c08caf76d2fa817)
|
||||
* docs: update CHANGELOG.md (#5417) by @github-actions[bot] in [`b96515e7d`](https://github.com/meshtastic/Meshtastic-Android/commit/b96515e7d048a0850f29cc5381326fd796a07e2c)
|
||||
* chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5416) by @James Rich in [`09a0d123b`](https://github.com/meshtastic/Meshtastic-Android/commit/09a0d123bed0e79415a8297c5cba92bab636d79e)
|
||||
* chore(deps): update core/proto/src/main/proto digest to 1c62540 (#5413) by @renovate[bot] in [`56077db6d`](https://github.com/meshtastic/Meshtastic-Android/commit/56077db6d93fd76ca2fa585f49eb63d2e91ad400)
|
||||
* chore(deps): update actions/checkout action to v6 (#5414) by @renovate[bot] in [`d4ff10ef3`](https://github.com/meshtastic/Meshtastic-Android/commit/d4ff10ef3b1551637e016e1f4bee4e5c2a98d645)
|
||||
* Actions: fix Flatpak source file patterns for jq (#5415) by @Austin in [`1c418a57e`](https://github.com/meshtastic/Meshtastic-Android/commit/1c418a57ea09b0c6af6fde22e9d0ddee16b53e7a)
|
||||
* docs: update CHANGELOG.md (#5412) by @github-actions[bot] in [`bce8bba90`](https://github.com/meshtastic/Meshtastic-Android/commit/bce8bba90a74bdc64353e28b5abeb11474017b74)
|
||||
* feat: add Compose Preview Screenshot Testing infrastructure (#5410) by @James Rich in [`85c840de3`](https://github.com/meshtastic/Meshtastic-Android/commit/85c840de32096a00aa7fac42fca0d3a5417d3961)
|
||||
* fix(ci): prevent literal 'false' task name in desktop release build (#5411) by @James Rich in [`7202994ab`](https://github.com/meshtastic/Meshtastic-Android/commit/7202994abee99dc16a54f38d34a7607c2a573663)
|
||||
* docs: update CHANGELOG.md (#5409) by @github-actions[bot] in [`1373bd330`](https://github.com/meshtastic/Meshtastic-Android/commit/1373bd330b4829ac29d37b0e1e088d21b66dc037)
|
||||
* feat(api): add hasAnyEntries method to local data sources and improve… (#5406) by @James Rich in [`10c5b5db2`](https://github.com/meshtastic/Meshtastic-Android/commit/10c5b5db2e5b23c52a3a5689205bfc42285b7b0a)
|
||||
* Update notification intents and deep link URI format (#5408) by @James Rich in [`82135df86`](https://github.com/meshtastic/Meshtastic-Android/commit/82135df865c4595d3ee509ac3535b86e343874b6)
|
||||
* docs: update CHANGELOG.md (#5399) by @github-actions[bot] in [`fd0e2c090`](https://github.com/meshtastic/Meshtastic-Android/commit/fd0e2c0904d21df2a962bd18ee138afabf869e98)
|
||||
* chore(deps): update core/proto/src/main/proto digest to 1c62540 (#5404) by @renovate[bot] in [`d84781848`](https://github.com/meshtastic/Meshtastic-Android/commit/d847818487787a854b59e06e29b7831aef45b514)
|
||||
* chore(deps): update ruby to v4.0.4 (#5407) by @renovate[bot] in [`84cc8ad0d`](https://github.com/meshtastic/Meshtastic-Android/commit/84cc8ad0debf620c614617cf32fcce971d48a87d)
|
||||
* chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5400) by @James Rich in [`313dc1c2f`](https://github.com/meshtastic/Meshtastic-Android/commit/313dc1c2f7734284a2c6822e2a62dc10c597bfb1)
|
||||
* fix: graceful HTTP error handling in scheduled-updates workflow (#5405) by @Copilot in [`b33738728`](https://github.com/meshtastic/Meshtastic-Android/commit/b337387287c8c4e0e3ec3c48cbf19ca39de1ea36)
|
||||
* feat(desktop): fix mac notifications, new desktop icons (#5403) by @James Rich in [`c77e03c5c`](https://github.com/meshtastic/Meshtastic-Android/commit/c77e03c5c1b49d9ce6684042dca684fca68a1e99)
|
||||
* Brownfield gap remediation: 28 tasks + intro commonMain migration (#5401) by @James Rich in [`95c3bc0bc`](https://github.com/meshtastic/Meshtastic-Android/commit/95c3bc0bce4a0d30228d9c411c76cedb818afc91)
|
||||
* ci: disable Flatpak source generation for PR and merge queue checks (#5402) by @James Rich in [`6eacee626`](https://github.com/meshtastic/Meshtastic-Android/commit/6eacee626b42133c742d8ca935590d7a4c94e3ee)
|
||||
* chore(deps): update core/proto/src/main/proto digest to b302d92 (#5398) by @renovate[bot] in [`f89fdaf99`](https://github.com/meshtastic/Meshtastic-Android/commit/f89fdaf99dccb46517458e7fecf840bcc9c86f44)
|
||||
* docs: update CHANGELOG.md (#5387) by @github-actions[bot] in [`8c0ced2ad`](https://github.com/meshtastic/Meshtastic-Android/commit/8c0ced2adcbd7a93aba109aeaab7405df57b03dd)
|
||||
* Gradle: Add flatpakGradleGenerator task (#5369) by @Austin in [`484518195`](https://github.com/meshtastic/Meshtastic-Android/commit/484518195e7dbf980de6b951410f23ebac11c237)
|
||||
* chore(deps): update core/proto/src/main/proto digest to 03eb534 (#5397) by @renovate[bot] in [`2d5bcc1e5`](https://github.com/meshtastic/Meshtastic-Android/commit/2d5bcc1e5093f21dbd73befe6dcfad4dac6b1cb6)
|
||||
* Release: Add MacOS ReleaseUberJar target (#5389) by @Austin in [`234a78a56`](https://github.com/meshtastic/Meshtastic-Android/commit/234a78a5640eb3979172b8523d49a1b35750e49c)
|
||||
* chore(deps): update vico to v3.2.0-next.4 (#5395) by @renovate[bot] in [`39c765c63`](https://github.com/meshtastic/Meshtastic-Android/commit/39c765c63e35afafb1b788d4ca3150b35c814348)
|
||||
* chore(deps): update fastlane to v2.234.0 (#5396) by @renovate[bot] in [`880e98abf`](https://github.com/meshtastic/Meshtastic-Android/commit/880e98abf05300a01f678bdee472b22b52d78058)
|
||||
* chore(deps): update org.jetbrains.kotlinx:kotlinx-datetime to v0.8.0-0.6.x-compat (#5382) by @renovate[bot] in [`879e147ef`](https://github.com/meshtastic/Meshtastic-Android/commit/879e147ef0a018b9d7b5214b70ec8e36a80e3d11)
|
||||
* chore(deps): update kotlinx.coroutines.android to v1.11.0 (#5393) by @renovate[bot] in [`78f9a8940`](https://github.com/meshtastic/Meshtastic-Android/commit/78f9a8940cdf4cbd83d78796ccc3a8aef4c4e5ac)
|
||||
* chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5383) by @James Rich in [`443de4a34`](https://github.com/meshtastic/Meshtastic-Android/commit/443de4a34d54d33a8e7d7ec46606b5fa70921a36)
|
||||
* docs: Update specs and governance for Android M3 accessibility (#5392) by @James Rich in [`0d195ead5`](https://github.com/meshtastic/Meshtastic-Android/commit/0d195ead53d5f88f73e581a7ee005f65bdceccfa)
|
||||
* docs(specs): add feature specs for discovery, node-list-layout, and app-docs (#5388) by @James Rich in [`c0d95d6ac`](https://github.com/meshtastic/Meshtastic-Android/commit/c0d95d6ac4196fcbc705f2d3f174c7d9c46a77b2)
|
||||
* chore(deps): update com.google.firebase:firebase-bom to v34.13.0 (#5385) by @renovate[bot] in [`934e687bc`](https://github.com/meshtastic/Meshtastic-Android/commit/934e687bc5c14b605797d2ca8f78c231fdfd5210)
|
||||
* docs: comprehensive copilot instructions & spec-kit setup (#5386) by @James Rich in [`031f25ced`](https://github.com/meshtastic/Meshtastic-Android/commit/031f25cedaf994b09bff014245ee8ab448aafa2a)
|
||||
* docs: update CHANGELOG.md (#5381) by @github-actions[bot] in [`aeff0a6ce`](https://github.com/meshtastic/Meshtastic-Android/commit/aeff0a6ce1b3a61669d55029ee04863c94711bc6)
|
||||
* chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5378) by @James Rich in [`924f8a507`](https://github.com/meshtastic/Meshtastic-Android/commit/924f8a507b257829ec15ac446ff31c26f1ecb6ac)
|
||||
* chore(deps): update aboutlibraries to v14.2.0 (#5380) by @renovate[bot] in [`d68aada1b`](https://github.com/meshtastic/Meshtastic-Android/commit/d68aada1ba9142efd088d9e205341322e267936d)
|
||||
* docs: update CHANGELOG.md (#5377) by @github-actions[bot] in [`3dd985d7d`](https://github.com/meshtastic/Meshtastic-Android/commit/3dd985d7dfaa53891eca4dcdc90f0330044d5106)
|
||||
* refactor(firmware): replace PlatformBackHandler with NavigationBackHandler (#5376) by @James Rich in [`213c38051`](https://github.com/meshtastic/Meshtastic-Android/commit/213c380514438e1e208f0a0439437d6e62b041f1)
|
||||
* docs: update CHANGELOG.md (#5374) by @github-actions[bot] in [`8d621bc6f`](https://github.com/meshtastic/Meshtastic-Android/commit/8d621bc6f48e167c3bdda1add22e2938dadf0bd5)
|
||||
* chore(deps): update paging to v3.5.0 (#5375) by @renovate[bot] in [`0b75fd414`](https://github.com/meshtastic/Meshtastic-Android/commit/0b75fd4140ed5643806fc8f1a6eb8c3dfaaeb049)
|
||||
* chore(deps): update compose-multiplatform to v1.11.1 (#5371) by @renovate[bot] in [`d28d684d0`](https://github.com/meshtastic/Meshtastic-Android/commit/d28d684d054fa1d82ec84ed0c2ecb3ddf4705766)
|
||||
* chore(deps): update androidx.camera.viewfinder:viewfinder-compose to v1.6.1 (#5370) by @renovate[bot] in [`2f2a71587`](https://github.com/meshtastic/Meshtastic-Android/commit/2f2a71587dd5ff36da025e5761159ff182371b81)
|
||||
* chore(deps): update vico to v3.2.0-next.5 (#5470) by renovate[bot] (@renovate[bot]) in [`4bacff81c`](https://github.com/meshtastic/Meshtastic-Android/commit/4bacff81c5abad536b40f7370ae5d302b74a11fe)
|
||||
* chore(deps): update spotless to v8.5.1 (#5468) by renovate[bot] (@renovate[bot]) in [`c91219d8b`](https://github.com/meshtastic/Meshtastic-Android/commit/c91219d8b0411975c059c7a5ae6eb370386954f6)
|
||||
* chore(deps): update wire to v6.4.0 (#5466) by renovate[bot] (@renovate[bot]) in [`f0e12695b`](https://github.com/meshtastic/Meshtastic-Android/commit/f0e12695bbf3df16b043476ff98856b727d6a2f7)
|
||||
* fix(database): make withDb retry logic resilient to varying close messages (#5474) by James Rich (@jamesarich) in [`057d5bb77`](https://github.com/meshtastic/Meshtastic-Android/commit/057d5bb778a72cf4359abc7fae904d587cdc90a2)
|
||||
* ci: remove desktop build job from reusable-check to cut macOS runner costs (#5475) by James Rich (@jamesarich) in [`d24fc9ac9`](https://github.com/meshtastic/Meshtastic-Android/commit/d24fc9ac9245b69718e2cb2a6c3a872e36d8c28c)
|
||||
* refactor(build): rename entry modules and remove DESKTOP_ONLY mode (#5476) by James Rich (@jamesarich) in [`f4b6b02ac`](https://github.com/meshtastic/Meshtastic-Android/commit/f4b6b02acecdd8855408db7d92757f2495fbc11f)
|
||||
* fix(settings): add input validation for BLE PIN, LoRa modem, and ambient lighting (#5477) by James Rich (@jamesarich) in [`1dd47bc09`](https://github.com/meshtastic/Meshtastic-Android/commit/1dd47bc09032fe0972af7eed6a7554e61be7a02b)
|
||||
* chore(deps): update markdownrenderer to v0.41.0 (#5471) by renovate[bot] (@renovate[bot]) in [`f6587a123`](https://github.com/meshtastic/Meshtastic-Android/commit/f6587a12364cc490b5872ed7bc77cfeed9e414a6)
|
||||
* fix(nav): remote admin nodenum + Nav3 consolidation and improvements (#5478) by James Rich (@jamesarich) in [`df4f10c4d`](https://github.com/meshtastic/Meshtastic-Android/commit/df4f10c4d64a273688aa9dbe5284ac3737756310)
|
||||
* chore(deps): update core/proto/src/main/proto digest to 59cb394 (#5480) by renovate[bot] (@renovate[bot]) in [`72436e70b`](https://github.com/meshtastic/Meshtastic-Android/commit/72436e70bc1db9373581bc53e8cc62548e596744)
|
||||
* feat: adopt Material 3 Expressive design system (M3-native APIs only) (#5479) by James Rich (@jamesarich) in [`f5128798a`](https://github.com/meshtastic/Meshtastic-Android/commit/f5128798a808219a28e9ee0916c079edcb203744)
|
||||
* feat(docs): In-app documentation browser with Jekyll site and Docusaurus sync (#5445) by James Rich (@jamesarich) in [`fc0df1a79`](https://github.com/meshtastic/Meshtastic-Android/commit/fc0df1a79ad1d8ce355803f5ac6eb4a1f2ec78c3)
|
||||
* chore(deps): update actions/checkout action to v6 (#5481) by renovate[bot] (@renovate[bot]) in [`2e484e219`](https://github.com/meshtastic/Meshtastic-Android/commit/2e484e219c132b89597ffa939e1801765a5d5352)
|
||||
* chore(deps): update actions/deploy-pages action to v5 (#5482) by renovate[bot] (@renovate[bot]) in [`d7cccd0db`](https://github.com/meshtastic/Meshtastic-Android/commit/d7cccd0dba442d8c8892a4aac8124f565563434a)
|
||||
* chore(deps): update actions/github-script action to v9 (#5483) by renovate[bot] (@renovate[bot]) in [`f8a5f894a`](https://github.com/meshtastic/Meshtastic-Android/commit/f8a5f894a86b6145b86143888f7ea1fe3d9ccbb6)
|
||||
* chore(deps): update io.nlopez.compose.rules:detekt to v0.5.9 (#5485) by renovate[bot] (@renovate[bot]) in [`4eb7ed8fe`](https://github.com/meshtastic/Meshtastic-Android/commit/4eb7ed8fe0853850a8056524403d4da3d8c36de3)
|
||||
* chore(deps): update actions/setup-node action to v6 (#5486) by renovate[bot] (@renovate[bot]) in [`733cb92ae`](https://github.com/meshtastic/Meshtastic-Android/commit/733cb92aecb51deab11edb2e9c3e198f9579304c)
|
||||
* chore(deps): update gradle/actions action to v6 (#5488) by renovate[bot] (@renovate[bot]) in [`21993b6cc`](https://github.com/meshtastic/Meshtastic-Android/commit/21993b6cc7eba8776262c3805942ddc460ab0c5b)
|
||||
* chore(deps): update actions/upload-pages-artifact action to v5 (#5487) by renovate[bot] (@renovate[bot]) in [`4afa1a032`](https://github.com/meshtastic/Meshtastic-Android/commit/4afa1a032fc465464e7408ef36d0505876ce9d61)
|
||||
* chore(deps): update actions/setup-java action to v5 (#5484) by renovate[bot] (@renovate[bot]) in [`bbdc4a300`](https://github.com/meshtastic/Meshtastic-Android/commit/bbdc4a300406ae417ad7a70fb4cdd826debb5cb3)
|
||||
* docs: comprehensive accuracy audit and CI fix (#5489) by James Rich (@jamesarich) in [`ece771edb`](https://github.com/meshtastic/Meshtastic-Android/commit/ece771edb06f0110050baac322a093214b89f6c8)
|
||||
* fix(ci): disable configuration cache for Dokka build (#5492) by James Rich (@jamesarich) in [`fe2cbae87`](https://github.com/meshtastic/Meshtastic-Android/commit/fe2cbae8756c4af9f1daacd842ab0fde097ffab6)
|
||||
* chore(deps): update compose-multiplatform to v1.3.0-beta01 (#5490) by renovate[bot] (@renovate[bot]) in [`3bfaa466a`](https://github.com/meshtastic/Meshtastic-Android/commit/3bfaa466afc01fe03cc69ba7437610441ba8bcf7)
|
||||
* chore(deps): update node to v24 (#5491) by renovate[bot] (@renovate[bot]) in [`cdf57ced8`](https://github.com/meshtastic/Meshtastic-Android/commit/cdf57ced8e9d6a6ce66a88f10da304a41b0ab4c1)
|
||||
* chore(deps): update peter-evans/create-pull-request action to v8 (#5493) by renovate[bot] (@renovate[bot]) in [`a141df437`](https://github.com/meshtastic/Meshtastic-Android/commit/a141df437298b039086e9084ec234f925ffa09b2)
|
||||
* chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5465) by James Rich (@jamesarich) in [`83bb1a31f`](https://github.com/meshtastic/Meshtastic-Android/commit/83bb1a31f75bfd2717428f2fed287eb1e830302f)
|
||||
* docs: update CHANGELOG.md (#5473) by github-actions[bot] (@github-actions[bot]) in [`3121ea09a`](https://github.com/meshtastic/Meshtastic-Android/commit/3121ea09a3abb43713f0a090dac5147b6ae76f84)
|
||||
* fix(docs): use locale subdirectory inside files/ instead of qualifier (#5494) by James Rich (@jamesarich) in [`418861d35`](https://github.com/meshtastic/Meshtastic-Android/commit/418861d356f1edf5ac0c4b00f3bfc761ad34e609)
|
||||
* fix(ci): unblock Dokka documentation generation (#5496) by James Rich (@jamesarich) in [`228765a15`](https://github.com/meshtastic/Meshtastic-Android/commit/228765a159b6f1498566d3fe6005f91b470761b4)
|
||||
* chore(deps): update compose-multiplatform to v1.11.2 (#5497) by renovate[bot] (@renovate[bot]) in [`619897de8`](https://github.com/meshtastic/Meshtastic-Android/commit/619897de85616eb9bebedf850970abd24571f705)
|
||||
* docs: update CHANGELOG.md (#5495) by github-actions[bot] (@github-actions[bot]) in [`51d2c7b15`](https://github.com/meshtastic/Meshtastic-Android/commit/51d2c7b15cf1b3520cfbef13b8dbcff0b72748e4)
|
||||
* fix(ci): exclude pre-release tags from docs-release workflow (#5499) by James Rich (@jamesarich) in [`92cfbaee9`](https://github.com/meshtastic/Meshtastic-Android/commit/92cfbaee9b8819804571ae15fd0815b8ab785bce)
|
||||
* docs: move English sources into docs/en/ locale folder (#5501) by James Rich (@jamesarich) in [`11bc37c96`](https://github.com/meshtastic/Meshtastic-Android/commit/11bc37c96878cd1d9a822c1dad658f43c9be6604)
|
||||
* chore(deps): update org.junit.vintage:junit-vintage-engine to v6.1.0 (#5503) by renovate[bot] (@renovate[bot]) in [`ae127a1e1`](https://github.com/meshtastic/Meshtastic-Android/commit/ae127a1e15bdbbae6395a4c21da6440297cdb13b)
|
||||
* chore(deps): update org.junit.platform:junit-platform-launcher to v6.1.0 (#5502) by renovate[bot] (@renovate[bot]) in [`2b7621221`](https://github.com/meshtastic/Meshtastic-Android/commit/2b76212217122604eeb14ba02ea45d93809773a0)
|
||||
* chore(deps): update room to v3.0.0-alpha05 (#5498) by renovate[bot] (@renovate[bot]) in [`59499e827`](https://github.com/meshtastic/Meshtastic-Android/commit/59499e82723bdef4f1116ae5996814d59ab85595)
|
||||
* docs: update CHANGELOG.md (#5500) by github-actions[bot] (@github-actions[bot]) in [`6c51c1ac1`](https://github.com/meshtastic/Meshtastic-Android/commit/6c51c1ac18cd61f3ab381335ecada8c9d614b7cb)
|
||||
* fix(ci): use GitHub compare API for changelog author resolution (#5504) by James Rich (@jamesarich) in [`4ff6f23c5`](https://github.com/meshtastic/Meshtastic-Android/commit/4ff6f23c5e5d074f319736a74e563f28bf65c127)
|
||||
* docs: update CHANGELOG.md (#5505) by github-actions[bot] (@github-actions[bot]) in [`f38536ae6`](https://github.com/meshtastic/Meshtastic-Android/commit/f38536ae61c8eaa99bcb315b4953bbfd90b7979c)
|
||||
* fix(database): update @Relation annotations for Room 3.0.0-alpha05 (#5507) by James Rich (@jamesarich) in [`01d32e15c`](https://github.com/meshtastic/Meshtastic-Android/commit/01d32e15c00c952ca5933fb3d29c08d3a3989c59)
|
||||
* chore(deps): update plugin flatpak-gradle-generator to v1.8.0 (#5506) by renovate[bot] (@renovate[bot]) in [`9260033e0`](https://github.com/meshtastic/Meshtastic-Android/commit/9260033e04f604a8f15bb3efce611bedc695d40b)
|
||||
* docs: update CHANGELOG.md (#5508) by github-actions[bot] (@github-actions[bot]) in [`6fb3776e0`](https://github.com/meshtastic/Meshtastic-Android/commit/6fb3776e0c2588392111fb4da89780f8bf597eb5)
|
||||
* build: reorder Maven repos to reduce flatpak generator 404s (#5509) by James Rich (@jamesarich) in [`930dee09e`](https://github.com/meshtastic/Meshtastic-Android/commit/930dee09e5b3f23d02d0dc5aae3eeea61974bada)
|
||||
* chore(deps): migrate takpacket-sdk to Maven Central coordinates (#5512) by James Rich (@jamesarich) in [`5c3b22584`](https://github.com/meshtastic/Meshtastic-Android/commit/5c3b22584e4d5d71c6813842033bd2ae17b0041b)
|
||||
* ci: reduce flatpak source generation thrash (#5513) by James Rich (@jamesarich) in [`3f64b5a0e`](https://github.com/meshtastic/Meshtastic-Android/commit/3f64b5a0ecf7cc9ed6e81068ab3067033a7f74f4)
|
||||
* chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5511) by James Rich (@jamesarich) in [`1dcb7b9d1`](https://github.com/meshtastic/Meshtastic-Android/commit/1dcb7b9d1a29fcdfad214fe74360130c597ee989)
|
||||
* docs: update CHANGELOG.md (#5510) by github-actions[bot] (@github-actions[bot]) in [`0803dd3ae`](https://github.com/meshtastic/Meshtastic-Android/commit/0803dd3ae8d6970ae8218dcfd6d0bb9b63866da9)
|
||||
* fix: prevent node details hang when device hardware API is unreachable (#5514) by James Rich (@jamesarich) in [`877909fa9`](https://github.com/meshtastic/Meshtastic-Android/commit/877909fa94b55758be9dd91e2bd11e160b3822ed)
|
||||
* chore(deps): update ruby to v4.0.5 (#5515) by renovate[bot] (@renovate[bot]) in [`d37e08fcc`](https://github.com/meshtastic/Meshtastic-Android/commit/d37e08fccdabd3f673e3eccf90ab1538f02fe867)
|
||||
* docs: update CHANGELOG.md (#5516) by github-actions[bot] (@github-actions[bot]) in [`f5e5fb6da`](https://github.com/meshtastic/Meshtastic-Android/commit/f5e5fb6da63ee85e64dbbe19a519a5a3deccd408)
|
||||
* fix(ci): exclude kotlinNativeBundleConfiguration from flatpak source generation (#5517) by James Rich (@jamesarich) in [`e13a4359e`](https://github.com/meshtastic/Meshtastic-Android/commit/e13a4359e188bbf02087cdb6253ea2b70c8200ed)
|
||||
* docs: update CHANGELOG.md (#5518) by github-actions[bot] (@github-actions[bot]) in [`54fafd225`](https://github.com/meshtastic/Meshtastic-Android/commit/54fafd225bddfee446cfea01dee6887e082acc79)
|
||||
* fix(ci): downgrade Ruby to 4.0.4 for ubuntu-24.04 compatibility (#5519) by James Rich (@jamesarich) in [`199827755`](https://github.com/meshtastic/Meshtastic-Android/commit/1998277555d2d3ac9f01f8af9f6c4a0652c169ad)
|
||||
* ci: unify GitHub Pages deployment (Dokka + docs site) (#5523) by James Rich (@jamesarich) in [`a6523acbb`](https://github.com/meshtastic/Meshtastic-Android/commit/a6523acbb88766c6a1401f13d4b23ab679bf3af7)
|
||||
* chore(deps): update koin.plugin to v1.0.0 (#5524) by renovate[bot] (@renovate[bot]) in [`8de0a1f16`](https://github.com/meshtastic/Meshtastic-Android/commit/8de0a1f167e498ccef999d3917023e1f34640a8c)
|
||||
* chore(flatpak): optimize CI source generation and clean up redundant tasks (#5525) by James Rich (@jamesarich) in [`7ae1b2681`](https://github.com/meshtastic/Meshtastic-Android/commit/7ae1b2681313da5a07217ea6e04a0c3dc4b79946)
|
||||
* fix(docs): compile Jekyll docs site and add root redirect in CI (#5526) by James Rich (@jamesarich) in [`f94358551`](https://github.com/meshtastic/Meshtastic-Android/commit/f94358551c7e41a2ea07ca97185fa8331b19f09c)
|
||||
* docs: update CHANGELOG.md (#5520) by github-actions[bot] (@github-actions[bot]) in [`95df50014`](https://github.com/meshtastic/Meshtastic-Android/commit/95df500142abd44f7b4d2d75ba28ab72f1fe77ac)
|
||||
|
||||
### Open Beta (v2.7.14-open.8)
|
||||
### Open Beta (v2.7.14-open.9)
|
||||
Changes since [`v2.7.13`](https://github.com/meshtastic/Meshtastic-Android/releases/tag/v2.7.13):
|
||||
|
||||
#### 🏗️ Features
|
||||
@@ -154,6 +154,12 @@ Changes since [`v2.7.13`](https://github.com/meshtastic/Meshtastic-Android/relea
|
||||
* fix(connections): improve BLE scan reliability and UI lifecycle by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5329
|
||||
* feat: event firmware easter egg with ambient branding by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5354
|
||||
* feat: align theme with Design Standards v1.3, remove contrast setting by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5355
|
||||
* feat(desktop): fix mac notifications, new desktop icons by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5403
|
||||
* Update notification intents and deep link URI format by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5408
|
||||
* fix: clarify position precision as ± radius by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5428
|
||||
* feat: TAK v2 protocol integration with zstd compression and full CoT type support by @thebentern in https://github.com/meshtastic/Meshtastic-Android/pull/5434
|
||||
#### 🖥️ Desktop
|
||||
* fix(desktop): keep Vico package to prevent bytecode verification errors by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5424
|
||||
#### 🛠️ Fixes
|
||||
* fix(strings): replace plurals by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/4596
|
||||
* fix: replace fdroid map_style_selection string by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/4598
|
||||
@@ -259,6 +265,9 @@ Changes since [`v2.7.13`](https://github.com/meshtastic/Meshtastic-Android/relea
|
||||
* fix(data): default new-node notifications off for event firmware by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5323
|
||||
* fix(network): resolve empty MQTT address and enforce TLS on default server by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5333
|
||||
* fix(mqtt): harden TLS enforcement, add user CA trust, and improve error diagnostics by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5365
|
||||
* fix: clamp future lastHeard timestamps to current time on ingestion by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5418
|
||||
* revert: Update retry settings in gradle-wrapper.properties by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5430
|
||||
* fix: update screenshots by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5435
|
||||
#### 📝 Other Changes
|
||||
* refactor(ui): compose resources, domain layer by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/4628
|
||||
* Add per-message transport method icons for new message format by @Kealper in https://github.com/meshtastic/Meshtastic-Android/pull/4643
|
||||
|
||||
@@ -20,7 +20,7 @@ Thank you for your interest in contributing to Meshtastic-Android! We welcome co
|
||||
- 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`.
|
||||
- Do **not** use the legacy `androidApp/src/main/res/values/strings.xml`.
|
||||
- **Definition:** Add strings to `core/resources/src/commonMain/composeResources/values/strings.xml`.
|
||||
- **Usage:**
|
||||
```kotlin
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# `:app`
|
||||
# `:androidApp`
|
||||
|
||||
## 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.
|
||||
The `:androidApp` 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
|
||||
|
||||
@@ -9,7 +9,7 @@ The `:app` module is the entry point for the Meshtastic Android application. It
|
||||
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.
|
||||
The core background service that manages long-running communication with the mesh radio. While it is declared in the `:androidApp` 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.
|
||||
@@ -24,34 +24,35 @@ The module primarily serves as a "glue" layer, connecting:
|
||||
<!--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
|
||||
:androidApp[androidApp]:::android-application
|
||||
:androidApp -.-> :core:ble
|
||||
:androidApp -.-> :core:common
|
||||
:androidApp -.-> :core:data
|
||||
:androidApp -.-> :core:database
|
||||
:androidApp -.-> :core:datastore
|
||||
:androidApp -.-> :core:di
|
||||
:androidApp -.-> :core:domain
|
||||
:androidApp -.-> :core:model
|
||||
:androidApp -.-> :core:navigation
|
||||
:androidApp -.-> :core:network
|
||||
:androidApp -.-> :core:nfc
|
||||
:androidApp -.-> :core:prefs
|
||||
:androidApp -.-> :core:proto
|
||||
:androidApp -.-> :core:service
|
||||
:androidApp -.-> :core:resources
|
||||
:androidApp -.-> :core:ui
|
||||
:androidApp -.-> :core:barcode
|
||||
:androidApp -.-> :core:takserver
|
||||
:androidApp -.-> :feature:intro
|
||||
:androidApp -.-> :feature:messaging
|
||||
:androidApp -.-> :feature:connections
|
||||
:androidApp -.-> :feature:map
|
||||
:androidApp -.-> :feature:node
|
||||
:androidApp -.-> :feature:settings
|
||||
:androidApp -.-> :feature:docs
|
||||
:androidApp -.-> :feature:firmware
|
||||
:androidApp -.-> :feature:wifi-provision
|
||||
:androidApp -.-> :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;
|
||||
@@ -116,7 +116,7 @@ configure<ApplicationExtension> {
|
||||
}
|
||||
|
||||
// Disable ABI splits for bundle builds or when explicitly requested via Gradle property.
|
||||
// Usage: ./gradlew :app:bundleGoogleRelease -Pmeshtastic.disableAbiSplits=true
|
||||
// Usage: ./gradlew :androidApp:bundleGoogleRelease -Pmeshtastic.disableAbiSplits=true
|
||||
val disableSplits = providers.gradleProperty("meshtastic.disableAbiSplits").map { it.toBoolean() }.getOrElse(false)
|
||||
|
||||
// Enable ABI splits to generate smaller APKs per architecture for F-Droid/IzzyOnDroid
|
||||
@@ -221,6 +221,7 @@ dependencies {
|
||||
implementation(projects.feature.map)
|
||||
implementation(projects.feature.node)
|
||||
implementation(projects.feature.settings)
|
||||
implementation(projects.feature.docs)
|
||||
implementation(projects.feature.firmware)
|
||||
implementation(projects.feature.wifiProvision)
|
||||
implementation(projects.feature.widget)
|
||||
@@ -277,6 +278,9 @@ dependencies {
|
||||
googleImplementation(platform(libs.firebase.bom))
|
||||
googleImplementation(libs.firebase.analytics)
|
||||
googleImplementation(libs.firebase.crashlytics)
|
||||
googleImplementation(libs.firebase.ai)
|
||||
googleImplementation(libs.firebase.ai.ondevice)
|
||||
googleImplementation(libs.mlkit.translate)
|
||||
|
||||
fdroidImplementation(libs.osmdroid.android)
|
||||
fdroidImplementation(libs.osmdroid.geopackage) { exclude(group = "com.j256.ormlite") }
|
||||
31
androidApp/src/fdroid/AndroidManifest.xml
Normal file
31
androidApp/src/fdroid/AndroidManifest.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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/>.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!--
|
||||
Required for writing TAK route data packages to ATAK's auto-import directory.
|
||||
Only declared for the F-Droid flavor — the Google Play flavor uses scoped
|
||||
storage (SAF / app-scoped cache) so this permission is not needed there
|
||||
and would violate Play policy.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.app.di
|
||||
|
||||
import org.koin.core.annotation.Module
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.feature.docs.ai.AIDocAssistant
|
||||
import org.meshtastic.feature.docs.ai.KeywordFallbackAssistant
|
||||
import org.meshtastic.feature.docs.translation.DocTranslationService
|
||||
import org.meshtastic.feature.docs.translation.NoOpDocTranslator
|
||||
|
||||
/** Provides keyword-only fallback AI assistant for the F-Droid flavor (no on-device model). */
|
||||
@Module
|
||||
class FdroidAiModule {
|
||||
@Single fun aiDocAssistant(fallback: KeywordFallbackAssistant): AIDocAssistant = fallback
|
||||
|
||||
@Single fun docTranslationService(): DocTranslationService = NoOpDocTranslator()
|
||||
}
|
||||
@@ -18,5 +18,5 @@ package org.meshtastic.app.di
|
||||
|
||||
import org.koin.core.annotation.Module
|
||||
|
||||
@Module(includes = [FDroidNetworkModule::class])
|
||||
@Module(includes = [FDroidNetworkModule::class, FdroidAiModule::class])
|
||||
class FlavorModule
|
||||
@@ -34,11 +34,13 @@ import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuGroup
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MenuDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Surface
|
||||
@@ -723,69 +725,71 @@ private fun FdroidMainMapFilterDropdown(
|
||||
mapFilterState: MapFilterState,
|
||||
mapViewModel: MapViewModel,
|
||||
) {
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
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()
|
||||
DropdownMenuGroup(shapes = MenuDefaults.groupShapes()) {
|
||||
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() },
|
||||
)
|
||||
}
|
||||
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
|
||||
val filterOptions = LastHeardFilter.entries
|
||||
val selectedIndex = filterOptions.indexOf(mapFilterState.lastHeardFilter)
|
||||
@@ -0,0 +1,470 @@
|
||||
/*
|
||||
* 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.ai
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.google.firebase.Firebase
|
||||
import com.google.firebase.ai.Chat
|
||||
import com.google.firebase.ai.DownloadStatus
|
||||
import com.google.firebase.ai.InferenceMode
|
||||
import com.google.firebase.ai.OnDeviceConfig
|
||||
import com.google.firebase.ai.OnDeviceModelStatus
|
||||
import com.google.firebase.ai.ai
|
||||
import com.google.firebase.ai.type.GenerativeBackend
|
||||
import com.google.firebase.ai.type.PublicPreviewAPI
|
||||
import com.google.firebase.ai.type.Tool
|
||||
import com.google.firebase.ai.type.content
|
||||
import org.meshtastic.feature.docs.ai.AIDocAssistant
|
||||
import org.meshtastic.feature.docs.data.DocBundleLoader
|
||||
import org.meshtastic.feature.docs.data.KeywordSearchEngine
|
||||
import org.meshtastic.feature.docs.model.AIDocAssistantResult
|
||||
import org.meshtastic.feature.docs.model.DocPage
|
||||
import org.meshtastic.feature.docs.model.DocsAiError
|
||||
|
||||
/**
|
||||
* Gemini on-device AI assistant for the Google flavor.
|
||||
*
|
||||
* Uses Firebase AI Logic hybrid SDK with [InferenceMode.ONLY_ON_DEVICE] for fast first-message answers, and a cloud
|
||||
* model with URL context grounding for follow-up conversation. Supported on Pixel 9+, Samsung Galaxy S25/S26, OnePlus
|
||||
* 13/15, and other devices with AICore.
|
||||
*
|
||||
* Context strategy: extracts only the **most relevant paragraphs** from each page (those containing query terms),
|
||||
* strips markdown formatting to maximize information density, and fits within the on-device token budget (~4K tokens).
|
||||
* This ensures fast, offline-capable answers even without cloud fallback.
|
||||
*
|
||||
* @see <a href="https://firebase.google.com/docs/ai-logic/hybrid/android/get-started">Firebase AI Logic Hybrid</a>
|
||||
*/
|
||||
@OptIn(PublicPreviewAPI::class)
|
||||
class GeminiNanoDocAssistant(private val searchEngine: KeywordSearchEngine, private val bundleLoader: DocBundleLoader) :
|
||||
AIDocAssistant {
|
||||
|
||||
/** On-device model for fast first-message answers grounded in bundled docs. */
|
||||
private val onDeviceModel by lazy {
|
||||
Firebase.ai(backend = GenerativeBackend.googleAI())
|
||||
.generativeModel(
|
||||
modelName = MODEL_NAME,
|
||||
systemInstruction = content { text(SYSTEM_INSTRUCTION) },
|
||||
onDeviceConfig = OnDeviceConfig(mode = InferenceMode.ONLY_ON_DEVICE),
|
||||
)
|
||||
}
|
||||
|
||||
/** Cloud model with URL context — fetches only from meshtastic.org and github.com/meshtastic. */
|
||||
private val groundedModel by lazy {
|
||||
Firebase.ai(backend = GenerativeBackend.googleAI())
|
||||
.generativeModel(
|
||||
modelName = MODEL_NAME,
|
||||
systemInstruction = content { text(SYSTEM_INSTRUCTION) },
|
||||
tools = listOf(Tool.urlContext()),
|
||||
)
|
||||
}
|
||||
|
||||
/** Active multi-turn chat session. Maintains conversation history across messages. */
|
||||
private var chatSession: Chat? = null
|
||||
private var messageCount = 0
|
||||
|
||||
override suspend fun isSupported(): Boolean = try {
|
||||
// Always supported on Google flavor — cloud model with Google Search grounding is always available.
|
||||
// On-device model provides faster/offline answers when available; check status to trigger download.
|
||||
val ext = onDeviceModel.onDeviceExtension
|
||||
val status = ext?.checkStatus()
|
||||
Logger.d(tag = TAG) { "On-device model status: $status" }
|
||||
when (status) {
|
||||
OnDeviceModelStatus.AVAILABLE -> true
|
||||
|
||||
OnDeviceModelStatus.DOWNLOADING -> true
|
||||
|
||||
OnDeviceModelStatus.DOWNLOADABLE -> {
|
||||
Logger.i(tag = TAG) { "Model downloadable — requesting download" }
|
||||
ext.download().collect { downloadStatus ->
|
||||
when (downloadStatus) {
|
||||
is DownloadStatus.DownloadStarted ->
|
||||
Logger.d(tag = TAG) { "Download started: ${downloadStatus.bytesToDownload} bytes" }
|
||||
|
||||
is DownloadStatus.DownloadInProgress ->
|
||||
Logger.d(tag = TAG) {
|
||||
"Download progress: ${downloadStatus.totalBytesDownloaded} bytes"
|
||||
}
|
||||
|
||||
is DownloadStatus.DownloadCompleted -> Logger.i(tag = TAG) { "Model download completed" }
|
||||
|
||||
is DownloadStatus.DownloadFailed ->
|
||||
Logger.w(tag = TAG) { "Model download failed: $downloadStatus" }
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> true // Cloud grounded model is always available even without on-device support.
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.w(tag = TAG) { "isSupported() check failed, using cloud only: ${e.message}" }
|
||||
true // Cloud grounded model is always available.
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
override suspend fun answer(question: String, currentPageId: String?): AIDocAssistantResult = try {
|
||||
val bundle = bundleLoader.load()
|
||||
val queryTerms = extractQueryTerms(question)
|
||||
|
||||
// Load all page content for full-text search ranking.
|
||||
val allContent = bundle.pages.associateWith { page -> bundleLoader.readPage(page.id)?.markdown.orEmpty() }
|
||||
|
||||
// Rank pages by relevance: full-text content search + keyword/title matching.
|
||||
val rankedPages = rankPagesByRelevance(queryTerms, bundle.pages, allContent)
|
||||
Logger.d(tag = TAG) { "Ranked pages: ${rankedPages.take(5).map { "${it.first.id}(${it.second})" }}" }
|
||||
|
||||
// Build compact context by extracting only relevant paragraphs.
|
||||
val contextResult = buildContext(currentPageId, queryTerms, rankedPages, allContent, MAX_CONTEXT_CHARS)
|
||||
Logger.d(tag = TAG) {
|
||||
"Context: ${contextResult.parts.size} pages, ${contextResult.totalChars} chars (budget $MAX_CONTEXT_CHARS)"
|
||||
}
|
||||
|
||||
val prompt = buildPrompt(question, contextResult.parts)
|
||||
Logger.d(tag = TAG) { "Prompt: ${prompt.length} chars, message #$messageCount" }
|
||||
|
||||
val chatResult = generateWithChat(prompt)
|
||||
messageCount++
|
||||
Logger.d(tag = TAG) { "Response (${chatResult.answer.length} chars): ${chatResult.answer.take(200)}" }
|
||||
|
||||
// Merge context pages with any pages mentioned by title in the response (à la Meshtastic-Apple).
|
||||
val mentionedPages =
|
||||
bundle.pages.filter { page ->
|
||||
page.id !in contextResult.usedPageIds && chatResult.answer.contains(page.title, ignoreCase = true)
|
||||
}
|
||||
val allSourcePages =
|
||||
contextResult.usedPageIds.mapNotNull { id -> bundle.pages.find { it.id == id } } + mentionedPages
|
||||
|
||||
AIDocAssistantResult.Success(
|
||||
answer = chatResult.answer,
|
||||
sourcePages = allSourcePages,
|
||||
usedOnDeviceModel = chatResult.usedOnDevice,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Logger.w(tag = TAG) { "Inference failed: ${e.message}" }
|
||||
val errorType =
|
||||
when {
|
||||
e.message?.contains("BUSY", ignoreCase = true) == true -> DocsAiError.Busy
|
||||
e.message?.contains("BATTERY", ignoreCase = true) == true -> DocsAiError.Busy
|
||||
e.message?.contains("BACKGROUND", ignoreCase = true) == true -> DocsAiError.Busy
|
||||
e.message?.contains("UNAVAILABLE", ignoreCase = true) == true -> DocsAiError.ModelUnavailable
|
||||
else -> DocsAiError.Unknown
|
||||
}
|
||||
val fallbackPages = searchEngine.selectForTokenBudget(question, maxChars = MAX_CONTEXT_CHARS)
|
||||
AIDocAssistantResult.Error(reason = errorType, suggestedPages = fallbackPages)
|
||||
}
|
||||
|
||||
override fun resetSession() {
|
||||
chatSession = null
|
||||
messageCount = 0
|
||||
Logger.d(tag = TAG) { "Chat session reset" }
|
||||
}
|
||||
|
||||
/** Result from [generateWithChat] indicating which model produced the answer. */
|
||||
private data class ChatResult(val answer: String, val usedOnDevice: Boolean)
|
||||
|
||||
/**
|
||||
* Uses the Chat API for multi-turn conversation. First message tries on-device for speed; all messages also go
|
||||
* through the cloud chat session to maintain conversation history for follow-ups.
|
||||
*/
|
||||
private suspend fun generateWithChat(prompt: String): ChatResult {
|
||||
val chat = chatSession ?: groundedModel.startChat().also { chatSession = it }
|
||||
|
||||
// First message: try on-device in parallel for speed, use cloud chat as primary.
|
||||
if (messageCount == 0) {
|
||||
val onDeviceAnswer =
|
||||
try {
|
||||
val response = onDeviceModel.generateContent(prompt)
|
||||
response.text?.trimEnd()
|
||||
} catch (e: Exception) {
|
||||
Logger.d(tag = TAG) { "On-device inference failed: ${e.message}" }
|
||||
null
|
||||
}
|
||||
|
||||
// If on-device gave a good answer, send it to chat as history context and return it.
|
||||
if (onDeviceAnswer != null && !looksLikeNoAnswer(onDeviceAnswer)) {
|
||||
// Still send to cloud chat so it has context for follow-ups (fire and forget).
|
||||
try {
|
||||
val groundedPrompt = prompt + MESHTASTIC_URL_HINT
|
||||
chat.sendMessage(groundedPrompt)
|
||||
} catch (e: Exception) {
|
||||
Logger.d(tag = TAG) { "Cloud chat seeding failed (non-fatal): ${e.message}" }
|
||||
}
|
||||
return ChatResult(answer = onDeviceAnswer, usedOnDevice = true)
|
||||
}
|
||||
}
|
||||
|
||||
// Use cloud chat (maintains full conversation history for follow-ups).
|
||||
val groundedPrompt = prompt + MESHTASTIC_URL_HINT
|
||||
val response = chat.sendMessage(groundedPrompt)
|
||||
return ChatResult(
|
||||
answer = response.text?.trimEnd() ?: "I wasn't able to generate a response.",
|
||||
usedOnDevice = false,
|
||||
)
|
||||
}
|
||||
|
||||
/** Heuristic: detect when the model says it can't find the answer in the provided docs. */
|
||||
private fun looksLikeNoAnswer(answer: String): Boolean {
|
||||
val lower = answer.lowercase()
|
||||
return lower.contains("not in the docs") ||
|
||||
lower.contains("not found in") ||
|
||||
lower.contains("i don't have information") ||
|
||||
lower.contains("i couldn't find") ||
|
||||
lower.contains("not covered in the documentation")
|
||||
}
|
||||
|
||||
private data class ContextResult(val parts: List<String>, val usedPageIds: Set<String>, val totalChars: Int)
|
||||
|
||||
/** Builds context parts from ranked pages within the given char budget. */
|
||||
private fun buildContext(
|
||||
currentPageId: String?,
|
||||
queryTerms: List<String>,
|
||||
rankedPages: List<Pair<DocPage, Int>>,
|
||||
allContent: Map<DocPage, String>,
|
||||
budget: Int,
|
||||
): ContextResult {
|
||||
val usedPageIds = mutableSetOf<String>()
|
||||
val contextParts = mutableListOf<String>()
|
||||
var totalChars = 0
|
||||
|
||||
// Current page gets priority.
|
||||
if (currentPageId != null) {
|
||||
val content = allContent.entries.find { it.key.id == currentPageId }
|
||||
if (content != null && content.value.isNotBlank()) {
|
||||
val pageBudget = MAX_PAGE_CHARS.coerceAtMost(budget)
|
||||
val extracted = extractRelevantContent(content.key.title, content.value, queryTerms, pageBudget)
|
||||
if (extracted.isNotBlank()) {
|
||||
contextParts.add(extracted)
|
||||
totalChars += extracted.length
|
||||
usedPageIds.add(currentPageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add relevant paragraphs from top-ranked pages within budget.
|
||||
for ((page, _) in rankedPages) {
|
||||
if (page.id in usedPageIds) continue
|
||||
if (totalChars >= budget) break
|
||||
val pageContent = allContent[page] ?: continue
|
||||
if (pageContent.isBlank()) continue
|
||||
|
||||
val snippetBudget = (budget - totalChars).coerceAtMost(MAX_SNIPPET_CHARS)
|
||||
if (snippetBudget < MIN_USEFUL_SNIPPET) break
|
||||
|
||||
val extracted = extractRelevantContent(page.title, pageContent, queryTerms, snippetBudget)
|
||||
if (extracted.isNotBlank()) {
|
||||
contextParts.add(extracted)
|
||||
totalChars += extracted.length
|
||||
usedPageIds.add(page.id)
|
||||
}
|
||||
}
|
||||
|
||||
return ContextResult(parts = contextParts, usedPageIds = usedPageIds, totalChars = totalChars)
|
||||
}
|
||||
|
||||
/** Extracts query terms from a question, filtering short/stop words. */
|
||||
private fun extractQueryTerms(question: String): List<String> = question
|
||||
.lowercase()
|
||||
.replace(Regex("[^\\p{L}\\p{N}\\s-]"), " ")
|
||||
.split(Regex("\\s+"))
|
||||
.filter { it.length >= 2 && it !in STOP_WORDS }
|
||||
.distinct()
|
||||
|
||||
/**
|
||||
* Extracts the most relevant content from a page: the paragraphs that contain query terms, with markdown formatting
|
||||
* stripped for maximum information density.
|
||||
*/
|
||||
private fun extractRelevantContent(
|
||||
title: String,
|
||||
markdown: String,
|
||||
queryTerms: List<String>,
|
||||
maxChars: Int,
|
||||
): String {
|
||||
val plainText = stripMarkdown(markdown)
|
||||
|
||||
// Split into paragraphs (double newline or section breaks).
|
||||
val paragraphs = plainText.split(Regex("\n{2,}")).map { it.trim() }.filter { it.length >= MIN_PARAGRAPH_LEN }
|
||||
|
||||
// Score each paragraph by how many query terms it contains.
|
||||
val scored =
|
||||
paragraphs.map { paragraph ->
|
||||
val lower = paragraph.lowercase()
|
||||
val hits = queryTerms.count { term -> lower.contains(term) }
|
||||
paragraph to hits
|
||||
}
|
||||
|
||||
// Take paragraphs with hits first (sorted by hits desc), then fill with top paragraphs for context.
|
||||
val withHits = scored.filter { it.second > 0 }.sortedByDescending { it.second }
|
||||
val withoutHits = scored.filter { it.second == 0 }
|
||||
|
||||
val result = StringBuilder("$title: ")
|
||||
for ((paragraph, _) in withHits + withoutHits) {
|
||||
if (result.length + paragraph.length + 1 > maxChars) {
|
||||
// Try to fit a truncated version if we have room.
|
||||
val remaining = maxChars - result.length - 1
|
||||
if (remaining > MIN_USEFUL_SNIPPET) {
|
||||
result.append(paragraph.take(remaining))
|
||||
}
|
||||
break
|
||||
}
|
||||
result.append(paragraph).append('\n')
|
||||
}
|
||||
return result.toString().trim()
|
||||
}
|
||||
|
||||
/** Strips markdown formatting to produce dense plain text. */
|
||||
private fun stripMarkdown(markdown: String): String = markdown
|
||||
.replace(Regex("^#{1,6}\\s+", RegexOption.MULTILINE), "") // headers
|
||||
.replace(Regex("\\[([^]]+)]\\([^)]+\\)"), "$1") // links → text
|
||||
.replace(Regex("!\\[([^]]*)]\\([^)]+\\)"), "$1") // images → alt
|
||||
.replace(Regex("[*_]{1,3}([^*_]+)[*_]{1,3}"), "$1") // bold/italic
|
||||
.replace(Regex("`{1,3}[^`]*`{1,3}"), "") // inline code
|
||||
.replace(Regex("^[>|\\-*+]\\s?", RegexOption.MULTILINE), "") // block quotes, lists
|
||||
.replace(Regex("\\|"), " ") // table pipes
|
||||
.replace(Regex("-{3,}"), "") // horizontal rules
|
||||
.replace(Regex(" {2,}"), " ") // collapse whitespace
|
||||
.trim()
|
||||
|
||||
/**
|
||||
* Ranks pages by relevance using full-text content search + keyword/title matching. Returns pages sorted by score
|
||||
* descending, filtering out zero-score pages.
|
||||
*/
|
||||
private fun rankPagesByRelevance(
|
||||
queryTerms: List<String>,
|
||||
pages: List<DocPage>,
|
||||
allContent: Map<DocPage, String>,
|
||||
): List<Pair<DocPage, Int>> = pages
|
||||
.map { page ->
|
||||
var score = 0
|
||||
val content = allContent[page]?.lowercase().orEmpty()
|
||||
|
||||
for (term in queryTerms) {
|
||||
if (content.contains(term)) score += CONTENT_MATCH_SCORE
|
||||
if (page.title.lowercase().contains(term)) score += TITLE_MATCH_SCORE
|
||||
if (page.keywords.any { it.lowercase().contains(term) }) score += KEYWORD_MATCH_SCORE
|
||||
if (page.aliases.any { it.lowercase().contains(term) }) score += ALIAS_MATCH_SCORE
|
||||
}
|
||||
|
||||
page to score
|
||||
}
|
||||
.filter { it.second > 0 }
|
||||
.sortedByDescending { it.second }
|
||||
|
||||
private fun buildPrompt(question: String, contextParts: List<String>): String {
|
||||
val context =
|
||||
if (contextParts.isNotEmpty()) {
|
||||
contextParts.joinToString("\n\n")
|
||||
} else {
|
||||
FALLBACK_CONTEXT
|
||||
}
|
||||
return """
|
||||
|Bundled app documentation:
|
||||
|$context
|
||||
|
|
||||
|User question: $question
|
||||
"""
|
||||
.trimMargin()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ChirpyAI"
|
||||
|
||||
/** Gemini 3.1 Flash-Lite — latest stable model (2026-05-07), free tier, supports grounding. */
|
||||
private const val MODEL_NAME = "gemini-3.1-flash-lite"
|
||||
|
||||
private const val SYSTEM_INSTRUCTION =
|
||||
"""You are Chirpy, the friendly AI assistant built into the Meshtastic Android app. You help users understand mesh networking, configure their Meshtastic nodes, troubleshoot connectivity issues, and get the most out of the Meshtastic ecosystem.
|
||||
|
||||
Personality: Helpful, concise, enthusiastic about mesh networking. Use short paragraphs. Include relevant emoji sparingly (📡 🔋 📍).
|
||||
|
||||
Knowledge sources (in priority order):
|
||||
1. Bundled app documentation provided as context below
|
||||
2. Official Meshtastic documentation at meshtastic.org/docs
|
||||
3. Official Meshtastic GitHub repositories (github.com/meshtastic)
|
||||
4. General LoRa/mesh networking knowledge
|
||||
|
||||
Guidelines:
|
||||
- Answer the user's question directly and helpfully
|
||||
- When the bundled docs cover the topic, cite them
|
||||
- When the bundled docs don't cover it, use your knowledge of official Meshtastic sources — don't refuse to help
|
||||
- Only reference official Meshtastic sources (meshtastic.org, github.com/meshtastic) — never cite random forums, blogs, or third-party sites
|
||||
- For firmware-specific or hardware-specific questions beyond app scope, point users to meshtastic.org/docs
|
||||
- Keep answers concise (2-4 short paragraphs max) unless the user asks for detail
|
||||
- If you're truly unsure about something Meshtastic-specific, say so honestly rather than guessing"""
|
||||
|
||||
/** Total context char budget — sized for on-device Nano (~4K tokens ≈ 10K chars for context + prompt). */
|
||||
private const val MAX_CONTEXT_CHARS = 8_000
|
||||
|
||||
/** Max chars for the current page (gets priority). */
|
||||
private const val MAX_PAGE_CHARS = 4_000
|
||||
|
||||
/** Max chars per additional page snippet. */
|
||||
private const val MAX_SNIPPET_CHARS = 2_000
|
||||
|
||||
/** Minimum useful snippet size — don't bother with tiny fragments. */
|
||||
private const val MIN_USEFUL_SNIPPET = 100
|
||||
|
||||
/** Minimum paragraph length to consider. */
|
||||
private const val MIN_PARAGRAPH_LEN = 20
|
||||
|
||||
// Scoring weights for page ranking
|
||||
private const val CONTENT_MATCH_SCORE = 3
|
||||
private const val TITLE_MATCH_SCORE = 10
|
||||
private const val KEYWORD_MATCH_SCORE = 7
|
||||
private const val ALIAS_MATCH_SCORE = 5
|
||||
|
||||
private val STOP_WORDS =
|
||||
setOf(
|
||||
"the",
|
||||
"is",
|
||||
"at",
|
||||
"in",
|
||||
"on",
|
||||
"to",
|
||||
"of",
|
||||
"an",
|
||||
"it",
|
||||
"do",
|
||||
"me",
|
||||
"my",
|
||||
"or",
|
||||
"if",
|
||||
"be",
|
||||
"as",
|
||||
"by",
|
||||
"so",
|
||||
"we",
|
||||
"he",
|
||||
"up",
|
||||
"no",
|
||||
"am",
|
||||
"us",
|
||||
)
|
||||
|
||||
private const val FALLBACK_CONTEXT =
|
||||
"Meshtastic is an open-source mesh networking platform for LoRa radios. " +
|
||||
"The app connects to Meshtastic devices via Bluetooth or WiFi to send messages, " +
|
||||
"share location, and manage mesh network settings like channels, nodes, and modules."
|
||||
|
||||
/** URLs appended to prompts for the cloud model to leverage URL context tool. Only official sources. */
|
||||
private const val MESHTASTIC_URL_HINT =
|
||||
"\n\nFor additional context, you may reference these official sources:" +
|
||||
"\n- https://meshtastic.org/docs/" +
|
||||
"\n- https://github.com/meshtastic/Meshtastic-Android" +
|
||||
"\n- https://github.com/meshtastic/firmware" +
|
||||
"\n- https://github.com/meshtastic/protobufs"
|
||||
}
|
||||
}
|
||||
@@ -19,5 +19,5 @@ package org.meshtastic.app.di
|
||||
import org.koin.core.annotation.Module
|
||||
import org.meshtastic.app.map.prefs.di.GoogleMapsKoinModule
|
||||
|
||||
@Module(includes = [GoogleNetworkModule::class, GoogleMapsKoinModule::class])
|
||||
@Module(includes = [GoogleNetworkModule::class, GoogleMapsKoinModule::class, GoogleAiModule::class])
|
||||
class FlavorModule
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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 android.content.Context
|
||||
import okio.FileSystem
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import org.koin.core.annotation.Module
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.app.ai.GeminiNanoDocAssistant
|
||||
import org.meshtastic.app.translation.MlKitDocTranslator
|
||||
import org.meshtastic.feature.docs.ai.AIDocAssistant
|
||||
import org.meshtastic.feature.docs.data.DocBundleLoader
|
||||
import org.meshtastic.feature.docs.data.KeywordSearchEngine
|
||||
import org.meshtastic.feature.docs.translation.DocTranslationCache
|
||||
import org.meshtastic.feature.docs.translation.DocTranslationService
|
||||
|
||||
/** Provides the on-device Gemini Nano AI assistant for the Google flavor. */
|
||||
@Module
|
||||
class GoogleAiModule {
|
||||
@Single
|
||||
fun aiDocAssistant(searchEngine: KeywordSearchEngine, bundleLoader: DocBundleLoader): AIDocAssistant =
|
||||
GeminiNanoDocAssistant(searchEngine, bundleLoader)
|
||||
|
||||
@Single
|
||||
fun docTranslationCache(context: Context): DocTranslationCache =
|
||||
DocTranslationCache(cacheDir = context.cacheDir.toOkioPath(), fileSystem = FileSystem.SYSTEM)
|
||||
|
||||
@Single fun docTranslationService(cache: DocTranslationCache): DocTranslationService = MlKitDocTranslator(cache)
|
||||
}
|
||||
@@ -14,16 +14,20 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
|
||||
package org.meshtastic.app.map.component
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuGroup
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MenuDefaults
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -52,55 +56,56 @@ import kotlin.math.roundToInt
|
||||
internal fun MapFilterDropdown(expanded: Boolean, onDismissRequest: () -> Unit, mapViewModel: MapViewModel) {
|
||||
val mapFilterState by mapViewModel.mapFilterStateFlow.collectAsStateWithLifecycle()
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(Res.string.only_favorites)) },
|
||||
onClick = { mapViewModel.toggleOnlyFavorites() },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = MeshtasticIcons.Favorite,
|
||||
contentDescription = stringResource(Res.string.only_favorites),
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Checkbox(
|
||||
checked = mapFilterState.onlyFavorites,
|
||||
onCheckedChange = { mapViewModel.toggleOnlyFavorites() },
|
||||
)
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(Res.string.show_waypoints)) },
|
||||
onClick = { mapViewModel.toggleShowWaypointsOnMap() },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = MeshtasticIcons.PinDrop,
|
||||
contentDescription = stringResource(Res.string.show_waypoints),
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Checkbox(
|
||||
checked = mapFilterState.showWaypoints,
|
||||
onCheckedChange = { mapViewModel.toggleShowWaypointsOnMap() },
|
||||
)
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(Res.string.show_precision_circle)) },
|
||||
onClick = { mapViewModel.toggleShowPrecisionCircleOnMap() },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = MeshtasticIcons.Lens,
|
||||
contentDescription = stringResource(Res.string.show_precision_circle),
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Checkbox(
|
||||
checked = mapFilterState.showPrecisionCircle,
|
||||
onCheckedChange = { mapViewModel.toggleShowPrecisionCircleOnMap() },
|
||||
)
|
||||
},
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuGroup(shapes = MenuDefaults.groupShapes()) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(Res.string.only_favorites)) },
|
||||
onClick = { mapViewModel.toggleOnlyFavorites() },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = MeshtasticIcons.Favorite,
|
||||
contentDescription = stringResource(Res.string.only_favorites),
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Checkbox(
|
||||
checked = mapFilterState.onlyFavorites,
|
||||
onCheckedChange = { mapViewModel.toggleOnlyFavorites() },
|
||||
)
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(Res.string.show_waypoints)) },
|
||||
onClick = { mapViewModel.toggleShowWaypointsOnMap() },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = MeshtasticIcons.PinDrop,
|
||||
contentDescription = stringResource(Res.string.show_waypoints),
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Checkbox(
|
||||
checked = mapFilterState.showWaypoints,
|
||||
onCheckedChange = { mapViewModel.toggleShowWaypointsOnMap() },
|
||||
)
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(Res.string.show_precision_circle)) },
|
||||
onClick = { mapViewModel.toggleShowPrecisionCircleOnMap() },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = MeshtasticIcons.Lens,
|
||||
contentDescription = stringResource(Res.string.show_precision_circle),
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Checkbox(
|
||||
checked = mapFilterState.showPrecisionCircle,
|
||||
onCheckedChange = { mapViewModel.toggleShowPrecisionCircleOnMap() },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
|
||||
val filterOptions = LastHeardFilter.entries
|
||||
val selectedIndex = filterOptions.indexOf(mapFilterState.lastHeardFilter)
|
||||
@@ -14,12 +14,16 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
|
||||
package org.meshtastic.app.map.component
|
||||
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuGroup
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MenuDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -58,38 +62,16 @@ internal fun MapTypeDropdown(
|
||||
)
|
||||
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) {
|
||||
googleMapTypes.forEach { (name, type) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(name) },
|
||||
onClick = {
|
||||
mapViewModel.setSelectedGoogleMapType(type)
|
||||
onDismissRequest() // Close menu
|
||||
},
|
||||
trailingIcon =
|
||||
if (selectedCustomUrl == null && selectedGoogleMapType == type) {
|
||||
{
|
||||
Icon(
|
||||
MeshtasticIcons.Check,
|
||||
contentDescription = stringResource(Res.string.selected_map_type),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (customTileProviders.isNotEmpty()) {
|
||||
HorizontalDivider()
|
||||
customTileProviders.forEach { config ->
|
||||
DropdownMenuGroup(shapes = MenuDefaults.groupShapes()) {
|
||||
googleMapTypes.forEach { (name, type) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(config.name) },
|
||||
text = { Text(name) },
|
||||
onClick = {
|
||||
mapViewModel.selectCustomTileProvider(config)
|
||||
onDismissRequest() // Close menu
|
||||
mapViewModel.setSelectedGoogleMapType(type)
|
||||
onDismissRequest()
|
||||
},
|
||||
trailingIcon =
|
||||
if (selectedCustomUrl == config.urlTemplate) {
|
||||
if (selectedCustomUrl == null && selectedGoogleMapType == type) {
|
||||
{
|
||||
Icon(
|
||||
MeshtasticIcons.Check,
|
||||
@@ -102,7 +84,31 @@ internal fun MapTypeDropdown(
|
||||
)
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
if (customTileProviders.isNotEmpty()) {
|
||||
DropdownMenuGroup(shapes = MenuDefaults.groupShapes()) {
|
||||
customTileProviders.forEach { config ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(config.name) },
|
||||
onClick = {
|
||||
mapViewModel.selectCustomTileProvider(config)
|
||||
onDismissRequest()
|
||||
},
|
||||
trailingIcon =
|
||||
if (selectedCustomUrl == config.urlTemplate) {
|
||||
{
|
||||
Icon(
|
||||
MeshtasticIcons.Check,
|
||||
contentDescription = stringResource(Res.string.selected_map_type),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(Res.string.manage_custom_tile_sources)) },
|
||||
onClick = {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user